Go — это компилируемый язык программирования со строгой статической типизацией, сборщиком мусора и встроенным менеджером пакетов. Он разработан с упором на многопоточное программирование.
💡
В основе идеологии#Go лежат минималистичность, простота синтаксиса, высокая скорость сборки и выполнения, удобные абстракции для написания многопоточного кода и эффективная утилизация всех доступных ядер процессора.
Стандартная библиотека
Несмотря на компактность, стандартная библиотека Go позволяет решать большинство повседневных задач без обращения к сторонним библиотекам. В ней есть:
- средства для простой и быстрой реализации серверов и клиентов (как TCP/UDP, так и HTTP);
- пакеты для сериализации/десериализации данных в популярные форматы;
- единый интерфейс для потокового ввода-вывода (пакет
io
); - вспомогательные интерфейсы и функции для обработки и оборачивания ошибок;
- пакет
testing
, предоставляющий инструменты для быстрого и удобного написания unit-тестов и бенчмарков из коробки; - свой язык шаблонов для кодогенерации и server-side-рендеринга HTML-страниц.
ООП
Парадигме ООП язык следует лишь частично, оставаясь мультипарадигмальным. Несколько ослабить строгую типизацию призван механизм interface (интерфейс). Он даёт возможность задать ограничения на тип в виде списка методов, которые тот должен реализовывать.
При этом нет необходимости раскрывать и даже явным образом указывать конкретную реализацию. Типизация в этом случае работает по принципу «утиной» (duck typing): «если что-то плавает как утка, крякает как утка и летает как утка, то это, скорее всего, и есть утка». Достаточно реализовать набор методов у типа, чтобы он начал автоматически удовлетворять всем интерфейсам с аналогичными сигнатурами методов:
Напомним четыре признака объектно-ориентированного программирования:
- Абстракция — возможность определить характеристики (свойства и методы) объекта, которые полностью описывают его возможности. В Go нет классов, но структуры с методами служат им неплохой заменой.
- Инкапсуляция — возможность скрыть реализацию объекта, предоставив пользователю некую спецификацию (интерфейс) взаимодействия с ним. Go даёт возможность задать область видимости (публичные/приватные) методов структур и позволяет спрятать реализацию.
- Наследование — возможность создания производных от родительского объекта, которые будут расширять или изменять свойства и поведение родителя. К сожалению, Go не реализует в полной мере механизм наследования, но есть встраивание — можно создавать типы на основе существующих.
- Полиморфизм — возможность одному и тому же фрагменту кода работать с разными типами данных. Это происходит, когда объект может вести себя как другой объект. В Go нет полиморфизма в классическом понимании, однако похожие действия можно реализовать с помощью интерфейсов. Интерфейс определяет список методов, которые должен реализовывать тип, чтобы удовлетворять данному интерфейсу. Это ослабляет строгую типизацию и позволяет передавать в параметрах разные типы данных, поддерживающие один и тот же интерфейс.
Рассмотрим язык Go с точки зрения функционального программирования.
- Функции высшего порядка — функции, которые могут в аргументах принимать другие функции и возвращать функции в качестве результата. В Go функции рассматриваются как значения и могут передаваться в другие функции, возвращаясь в виде результата.
- Замыкания. Go позволяет определять и использовать функции, которые ссылаются на переменные своей родительской функции.
- Чистые функции. В Go можно определять функции, которые зависят только от входящих аргументов и не влияют на глобальное состояние.
- Рекурсия. Как и в большинстве языков, в Go можно применять рекурсивные вызовы функций.
- Ленивые вычисления. В Go нет поддержки ленивых (отложенных) вычислений.
- Иммутабельность переменных. В Go переменные могут изменять своё значение, поэтому иммутабельность (неизменяемость) переменных отсутствует.
Видно, что Go полностью не реализует парадигмы объектно-ориентированного и функционального программирования, но частично это компенсируется похожими возможностями. Поэтому Go считается мультипарадигмальным языком программирования.
Exceptions
Обработке ошибок в Go нашлось особое место. Во многих других языках ошибки обрабатываются с помощью механизма исключений (exceptions). Если в ходе выполнения функции происходит ошибка, выбрасывается специальное событие, называемое исключением, которое будет либо обработано тут же, в месте вызова функции, либо проброшено вверх по стеку, пока его кто-нибудь не поймает. Для «ловли» этого события нужна конструкция try — catch
— обработчик исключений:
У этого подхода есть недостаток: выброс исключения происходит неявно для вызывающего функцию кода, поэтому программисту приходится запоминать, какая функция может выбросить исключение. Также существуют uncatchable-исключения — например, относящиеся к выходу программы за пределы доступной ей памяти.
В Go применяется другой подход, который вносит больше ясности в процесс обработки ошибок. Дело в том, что функции в Go могут возвращать больше одного значения. Этим свойством активно пользуются разработчики, используя в качестве последнего возвращаемого значения интерфейс error
:
Тип error
в Go — встроенный, то есть, чтобы им воспользоваться, не нужно импортировать какой-либо пакет. Наличие отдельного типа позволяет одинаково обрабатывать ошибки, независимо от того, с какой функцией вы работаете — стандартной или сторонней библиотеки. Вы всегда работаете с одним и тем же интерфейсом error
, а значит, можете сравнить две ошибки или применить к ним функции интроспекции ошибок из библиотеки errors
.
Так как возможность возникновения ошибки при выполнении функции отражена непосредственно в её сигнатуре (последнее возвращаемое значение имеет тип error
), пользователь, который обращается к этой функции, вынужден всегда обрабатывать или игнорировать ошибку явно, иначе код не скомпилируется. Добавлять последним (обычно вторым) возвращаемым аргументом ошибку принято везде, где только может произойти ошибка. Чаще всего речь о функциях, в теле которых происходят операции ввода-вывода.
Panic
Также в Go существует механизм паники (panic). Если конструкция выше — типичный способ проверить выполнение той или иной функции, то паника выбрасывается только тогда, когда исполняющий код попадает в нестандартную ситуацию, которую невозможно обработать.
Go даёт возможность перехватывать и обрабатывать панику. Для этого используется конструкция defer
и встроенная функция recover
.
defer
— это ещё одна необычная концепция языка, которая выполняет блоки кода при выходе из функции, например, чтобы закрывать файлы по завершении работы с ними. Можно рассматривать defer
как замену деструкторов/менеджеров контекста в других языках (try_with_resources
из Java, with
из Python). defer
выполняется даже в случае паники, когда происходит аварийное завершение функций.
Может показаться, что паника очень похожа на механизм исключений, но это не так. Выбрасывая exception
, функция обычно ждёт, что исключение будет поймано выше обработчиком исключений. Однако перехват паники происходит не всегда. Если в случае паники, при возвращении управления назад по стеку функции, не будет вызвана recover()
, то программа завершит свою работу с ошибкой.
Аварийную ситуацию можно создать самостоятельно, вызвав функцию panic
с параметром любого типа. По умолчанию паника будет идти вверх по стеку и завершать все функции, пока не завершит функцию main
, а вместе с ней и весь процесс. К использованию функции panic()
следует относиться с осторожностью, о чём напоминает постулат «Не паниковать» (Don’t panic). Не нужно использовать панику там, где можно просто возвратить ошибку и затем обработать её.
recover
— функция, которая позволяет восстановить выполнение программы в случае паники. Если на момент вызова recover
произошла аварийная ситуация, то recover
завершает её и возвращает значение ошибки (аргумент при вызове panic
). Если аварийной ситуации не было, recover
ничего не делает и возвращает nil
.
Инструменты для тестирования
В Go принято располагать файлы с unit-тестами непосредственно в пакете, функции которого вы тестируете. Например, если код, который вы хотите покрыть тестами, располагается в файле foo.go
, для тестов нужно создать файл foo_test.go
:
foo/ # пакет foo
foo.go # файл с тестируемым кодом
foo_test.go # файл с тестами
Содержимое файла foo.go
:
В файле foo_test.go
реализуем функции определённой сигнатуры:
Выполнить их можно, просто вызвав команду go test
.
Какие элементы многопоточности есть в Go?
Как было сказано ранее, многопоточность в Go реализована согласно модели CSP (Communicating Sequential Processes). При таком подходе программа представляет собой множество одновременно работающих подзадач, которые общаются с помощью каналов связи. Задачами в Go выступают горутины (goroutine), связь организована через каналы (channel).
Изначально в Go была реализована кооперативная многозадачность: пока код в горутине сам не передаст управление (например, попытавшись выполнить блокирующую операцию), забрать управление у этой горутины невозможно. С версии 1.14 планировщик стал в том числе вытесняющим. Вытесняющий планировщик самостоятельно распределяет процессорное время между горутинами.
Горутина — это легковесный поток, который занимает гораздо меньше памяти, чем поток ОС. Среда выполнения Go может выполнять несколько горутин на одном потоке операционной системы и быстро переключаться с выполнения одной горутины на другую благодаря их малому размеру. Вытесняющий планировщик старается равномерно распределять процессорное время между горутинами.
Каналы — это второй ключевой элемент в многопоточности на Go. Каналы не только дают возможность потокам обмениваться данными, но и служат для синхронизации их работы. Одна горутина может записать данные в канал, а другая горутина прочитать их. Кроме этого, стандартная библиотека имеет дополнительные примитивы синхронизации потоков.
Горутины, каналы и примитивы синхронизации делают язык Go чрезвычайно удобным инструментом для создания многопоточных программ и различных сервисов.
📂 Go | Последнее изменение: 12.08.2024 19:53