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