Для языков программирования Go довольно молод. Впервые он был выпущен 10 ноября 2009 года. Его создатели Роберт Гризмер
Роб Пайк и Кен Томпсон работали в Google, где проблема массового масштабирования вдохновила их на создание Go как быстрого и эффективного решения для программирования проектов с большой кодовой базой, управляемых несколькими разработчиками, предъявляющих жесткие требования к производительности и охватывающих несколько сетей и вычислительных ядер.
Создатели Go также воспользовались возможностью при создании своего нового языка изучить сильные и слабые стороны и недостатки других языков программирования. В результате получился чистый, понятный и практичный язык с относительно небольшим набором команд и возможностей.
В этой статье я расскажу о 10 особенностях Go, которые (по моим личным наблюдениям) отличают его от других языков.
1. Go всегда включает бинарные файлы в сборки
Среда выполнения Go предоставляет такие услуги, как распределение памяти, сборка мусора, поддержка параллелизма и работа в сети. Она компилируется в каждый двоичный файл Go. Это отличается от многих других языков, многие из которых используют виртуальную машину, которую необходимо установить вместе с программой для корректной работы.
Включение среды выполнения непосредственно в двоичный файл делает распространение и запуск программ на Go чрезвычайно простым и позволяет избежать проблем несовместимости между средой выполнения и программой. Виртуальные машины таких языков, как Python, Ruby и JavaScript, также не оптимизированы для сборки мусора и распределения памяти, что объясняет превосходную скорость Go по сравнению с другими подобными языками. Например, Go хранит максимум возможного в стеке, где данные выстраиваются последовательно и доступ к ним гораздо быстрее, чем в куче. Подробнее об этом позже.
И последнее о статических двоичных файлах Go: поскольку для их запуска не требуется никаких внешних зависимостей, они запускаются невероятно быстро. Это полезно, если вы используете такую службу, как Google App Engine, платформу как сервис, работающую в Google Cloud, которая может масштабировать ваше приложение до нуля экземпляров для экономии облачных затрат. Когда поступает новый запрос, App Engine может в мгновение ока запустить экземпляр вашей программы на Go. Аналогичный опыт в Python или Node обычно приводит к 3-5 секундному ожиданию (или дольше), поскольку вместе с новым экземпляром запускается и необходимое виртуальное окружение.
2. В Go нет централизованного сервиса для хранения зависимостей программ
Чтобы получить доступ к опубликованным программам Go, разработчики не полагаются на централизованно размещенную службу, как Maven Central для Java или реестр NPM для JavaScript. Скорее, проекты распространяются через репозитории исходного кода (чаще всего Github). Командная строка go install позволяет загружать репозитории таким образом.
Почему мне нравится эта функция? Я всегда считал централизованные службы зависимостей, такие как Maven Central, PIP и NPM, несколько пугающими черными ящиками, которые, возможно, абстрагируют от хлопот, связанных с загрузкой и установкой зависимостей (и зависимостей зависимостей), но неизбежно вызывают пугающую остановку сердца при возникновении ошибки зависимости (я пережил их слишком много, чтобы сосчитать).
Часто меня расстраивало то, что я так и не смог до конца понять, как они работают под капотом. Отказавшись от центрального сервиса, процесс установки, версионирования и управления зависимостями вашего проекта Go становится очень понятным и, следовательно, более чистым.
Кроме того, сделать ваш модуль доступным для других так же просто, как поместить его в систему контроля версий, что является привлекательно простым способом распространения ваших программ.
3. Go - это вызов по значению
В Go, когда вы передаете примитив (число, булеву или строку) или struct (грубый эквивалент объекта класса) в качестве параметра функции, Go всегда создает копию значения переменной.
Во многих других языках, таких как Java, Python и JavaScript, примитивы передаются по значению, но объекты (экземпляры классов) передаются по ссылке, то есть принимающая функция фактически получает указатель на исходный объект, а не его копию.
Это означает, что любые изменения, внесенные в объект в принимающей функции, отражаются в исходном объекте.
Приведенная выше функция получает указатель на Foo и мутирует исходный объект.
Это четкое различие между вызовом по значению и вызовом по ссылке делает ваши намерения очевидными и уменьшает вероятность того, что вызывающая функция случайно мутирует переданный объект, когда она не должна этого делать (то, с чем многие начинающие разработчики не могут разобраться).
Как резюмирует MIT: "Мутабельность усложняет понимание того, что делает ваша программа, и гораздо сложнее обеспечить выполнение контрактов".
Более того, call-by-value значительно сокращает работу сборщика мусора, что означает более быстрые и эффективные с точки зрения памяти приложения. В этой статье делается анекдотический вывод о том, что поиск указателей (извлечение значений указателей из кучи) в 10-20 раз медленнее, чем извлечение значения из непрерывного стека. Хорошим эмпирическим правилом является следующее: самый быстрый способ чтения из памяти - это последовательное чтение, а это означает сокращение до минимума количества указателей, случайно хранящихся в оперативной памяти.
4. Ключевое слово 'defer'
В NodeJS, до того как я начал использовать knex.js, я вручную управлял соединениями с базами данных в своем коде, создавая пул БД, а затем открывая новое соединение из пула в каждой функции, освобождая соединение в конце функции, когда необходимая CRUD-функция базы данных была завершена.
Это был кошмар в плане обслуживания, потому что если я не освобождал соединение в конце каждой функции, количество неосвобожденных соединений с БД медленно росло, пока в пуле не оставалось свободных соединений, после чего приложение выходило из строя.
Реальность такова, что программам часто приходится освобождать, очищать и разрывать ресурсы, файлы, соединения и т.д., поэтому Go ввел ключевое слово defer как эффективный способ управления этим.
Любое утверждение, которому предшествует слово defer, откладывает его вызов до тех пор, пока не завершится окружающая функция. Это означает, что вы можете поместить свой код очистки/разрушения в начало функции (где он очевиден), зная, что он сделает свое дело после завершения функции.
В приведенном выше примере метод закрытия файла является отложенным. Мне нравится эта схема, когда вы объявляете о своем намерении по уборке в верхней части функции, а затем забываете о нем, зная, что оно сделает свою работу после завершения функции.
5. Go перенимает лучшие черты функционального программирования
Функциональное программирование - это эффективная и творческая парадигма, и, к счастью, Go перенимает лучшие черты функционального программирования. В Go:
- функции являются значениями, то есть они могут добавляться в качестве значений в карту, передаваться в качестве параметров в другие функции, устанавливаться в переменные и возвращаться из функций (это называется "функции высшего порядка" и часто используется в Go для создания промежуточного программного обеспечения с помощью паттерна декоратора).
- анонимные функции могут быть созданы и автоматически вызываться.
- функции, объявленные внутри других функций, допускают замыкания (когда функции, объявленные внутри функций, могут обращаться к переменным, объявленным во внешней функции, и изменять их). В идиоматическом Go замыкания широко используются для ограничения области действия функции и для создания состояния, которое функции затем используют в своей логике.
Выше приведен пример закрытия. Функция 'StartTimer' возвращает новую функцию, которая благодаря закрытию имеет доступ к значению 't', заданному в области ее рождения. Эта функция может сравнивать текущее время со значением 't', создавая тем самым полезный таймер. Спасибо Мэту Райеру за этот пример.
6. В Go есть неявные интерфейсы
Каждый, кто читал литературу по SOLID-кодированию и паттернам проектирования, наверняка слышал мантру "Предпочитайте композицию наследованию". Вкратце это означает, что вы должны разбить свою бизнес-логику на различные интерфейсы, а не полагаться на иерархическое наследование свойств и логики от родительского класса.
Другой популярный принцип - "Программируйте на интерфейс, а не на реализацию": API должен публиковать только контракт ожидаемого поведения (сигнатуры методов), но не детали того, как это поведение реализуется.
Оба эти правила указывают на критическую важность интерфейсов в современном программировании.
Поэтому неудивительно, что в Go есть поддержка интерфейсов. Фактически, оказалось, что интерфейсы - это единственный абстрактный тип в Go.
Однако, в отличие от других языков, интерфейсы в Go реализуются не явно, а скорее неявно. Конкретный тип не объявляет, что он реализует интерфейс. Скорее, если набор методов конкретного типа содержит все наборы методов базового интерфейса, Go считает, что объект реализует интерфейс.
Такая неявная реализация интерфейса (формально называемая структурной типизацией) позволяет Go обеспечить безопасность типов и развязку, сохраняя при этом большую часть гибкости, присущей динамическим языкам.
Явные интерфейсы, напротив, связывают клиента и реализацию вместе, что делает замену зависимости в Java, например, гораздо более сложной, чем в Go.
В LogicProvider ничего не объявлено, чтобы указать, что он соответствует интерфейсу Logic. Это означает, что клиент может легко заменить своего поставщика логики в будущем, если только этот поставщик логики содержит все наборы методов базового интерфейса (Logic).
7. Обработка ошибок
Ошибки в Go обрабатываются совсем не так, как в других языках. Вкратце, Go обрабатывает ошибки, возвращая значение типа error в качестве последнего возвращаемого значения функции.
Если функция выполняется как ожидалось, для параметра ошибки возвращается nil, в противном случае возвращается значение ошибки. Вызывающая функция затем проверяет возвращаемое значение ошибки и обрабатывает ошибку, либо выбрасывает собственную ошибку.
Есть причины, по которым Go работает именно так: это заставляет программистов думать об исключениях и обрабатывать их должным образом. Традиционные исключения try-catch также добавляют по крайней мере один новый путь в коде и делают отступы в коде, которые могут быть сложными для понимания. Go предпочитает рассматривать "счастливый путь" как код без отступов, при этом любые ошибки выявляются и возвращаются до завершения "счастливого пути".
8. Concurrency
Возможно, самая известная функция Go, параллельность позволяет выполнять обработку параллельно на всех доступных ядрах машины или сервера. Параллельность имеет смысл, когда отдельные процессы не зависят друг от друга (не должны выполняться последовательно) и когда временная производительность критична. Это часто бывает в случае с требованиями ввода-вывода, когда чтение или запись на диск или в сеть происходит на несколько порядков медленнее, чем все, кроме самых сложных процессов в памяти.
Ключевое слово 'go' перед вызовом функции приводит к одновременному выполнению этой функции.
Параллельность в Go - это глубокая и довольно продвинутая функция, но там, где она имеет смысл, она предоставляет мощный способ обеспечить оптимальную производительность вашей программы.
9. Стандартная библиотека Go
Go придерживается философии "батарейки в комплекте", и многие требования современного языка программирования заложены в стандартную библиотеку, что значительно упрощает жизнь программистов.
Как уже упоминалось, тот факт, что Go - относительно молодой язык, означает, что многие проблемы/требования современных приложений учтены в стандартной библиотеке.
Например, Go предлагает поддержку мирового класса для работы с сетями (в частности, HTTP/2) и управления файлами. Он также предлагает встроенную кодировку и декодирование JSON. В результате настройка сервера для обработки HTTP-запросов и возврата ответов (JSON или других) чрезвычайно проста, что объясняет популярность Go для разработки HTTP-веб-сервисов на основе REST.
Как отмечает Мэт Райер, библиотека Standard с открытым исходным кодом - отличный способ изучить передовые методы работы в Go.
10. Отладка: игровая площадка Go
Отладка в любом языке - важнейшее требование. Большинство языков полагаются на сторонние онлайн-инструменты или умные IDE, предоставляющие инструменты отладки, которые позволяют разработчикам быстро проверить свой код. Go предоставил Go Playground - https://play.golang.org бесплатный онлайн-инструмент, где вы можете опробовать и поделиться небольшими программами. Это очень полезный инструмент, который превращает отладку в простое занятие.