Skip to content

Почему Go лучший язык программирования

Раньше мы говорили почему язык Go плох, по большей части касаясь его дизайна. Сегодня мы разберем, почему Go - лучший язык для вашего следующего проекта.

Single binary

Основное, как для меня, достоинство языка - он компилируемый и отдает один бинарный файл. Статически слинкованый. Это значит, при запуске вам не нужны интерпретаторы, virtual env-ы, node_modules, ставить дополнительно библиотеки, sudo apt install libkal-dev. Непонятно, почему остальные языки не пришли к этому же. Сам компилятор Go существует (в отличие от компилятора C/C++, на которые есть стандарты и частичные их реализации). Компилятор прост в использовании, быстр, удобен.

Отдельно стоит упомянуть, за счет чего достигается кросплатформенность. Это не новая виртуальная машина или интерпретатор, это известная со времен динозавров техника под названием кросскомпиляция. Вы компилируете код под нужную платформу, и используете полученный бинарник на этой платформе.

Simple language, syntax and semantics

Сам язык без преувеличения, простой. Заключается в этом то, что весь набор ключевых слов:

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var
Спеку можно прочитать за день, выучить язык по Tour of Go максимум неделю. После этого не надо изучать никакие паттерны проектирования, архитектуры, чистые архитектуры, RAII и т.д., чтобы полноценно пользоваться языком. Сам язык является слабо типизированным, в том смысле, что по большей части код покрыт типами и корректность проверяется в компайл-тайме, и для некоторых случаев - FFI, unsafe код, анмаршал JSON существуют "стертые" типы uintptr, interface{}, unsafe.Pointer. В общем это достаточно хороший "компромисс" между C и Java: есть указатели, есть примитивные типы для интов, флотов и булов, а также есть стандартные типы для строк, слайсов и мап.

Также язык ортогонален. Это значит, что его возможности не повторяются друг между другом, что приводит к тому, что один и тот же алгоритм в Go, написанный разными программистами будет выглядеть +- одинаково. Также это хорошо, потому что вам не нужно думать, использовать ли статический диспатч, динамический диспатч, наследование, дефолтные аргументы или делегаты, потому что этого всего нет и язык от этого несильно страдает, а код становится читабельнее, вместо этого можно сконцентрироваться на бизнес логике, и с удивлением обнаружить, что в конце написания очередной фичи - она работает с минимальными проблемами. Почему с удивлением - после языков с широким использованием проверок в рантайме: динамической типизацией и эксепшенов, в Java/Python/Javascript/etc. такой опыт получить сложнее.

Optional memory management

Язык предполагает Garbage collector поэтому заботиться о выделении и освобождении памяти не нужно. Для случаев, когда все-таки необходимо ограничить использование памяти, для этого есть все средства: сырые указатели, sync.Pool, арены, арены написанные руками, аллокаторы написанные руками. В целом, это высокоуровневый язык, позволяющий тем не менее пользоваться низкоуровневыми возможностями системы.

Rich standard library

Стандартная библиотека включает в себя всё, что может понадобиться, для относительно простых приложений, и в принципе можно обойтись без сторонних библиотек в некоторых случаях. Есть библиотека для работы с json, http сервер/клиент, шаблонизация, интерфейсы баз данных, взаимодействие с операционной системой, библиотека для работы с аргументами командной строки, профилировщик, рефлексия и т.д.

Testing

Отдельно стоит отметить, что язык Go поддерживает из коробки тесты с помощью стандартного пакета testing и команды go test.

Package management

Говоря про использование чужого кода, в Go есть развитая система пакетов, пользоваться которой тривиально - указываем путь до репозитория с модулем и делаем go mod tidy - готово. Импорты пакетов работают относительно их модуля, и этот подход гораздо проще, чем в Rust, где надо разбираться в module/super/import/crate и т.д., проще, чем в C/C++, в которых нет стандартных систем сборки, и если что-то и используется, то это сборщик команды компилятора, перечисляющий все необходимые файлы, хедеры, и пути для поиска чужих хедеров и обьектных файлов. Даже в том же Python/Java/etc нету стандартных менеджеров пакетов, только созданные сообществом, и пользоваться ими крайне неудобно, постоянно что-то ломается, загадка, как найти неиспользуемые зависимости или обновить старые.

No exceptions, errors are values

Модель обработки ошибок выбрана самая простая и вместе с тем (относительно) удобная - ошибки это значения. Функция, допускающая ошибку, просто возвращает результат или ошибку через механизм множественного возврата. На вызывающей стороне при этом ошибка должна быть обработана, иначе не сойдется тайп-чекинг: n := strconv.Atoi("123") не скомпилируется, т.к. ошибка ничему не присваивается. Тем самым язык заставляет обрабатывать ошибку, хотя бы элементарными механизмами типо: - игнорирование: n, _ := strconv.Atoi("123") - паника или assert:

// checkErr := func(err error) { if err != nil { panic(err) } }
n, err := strconv.Atoi("123")
checkErr(err)
- возврат ошибки в качестве результата:
n, err := strconv.Atoi("123")
if err != nil {
  return 0, err
}
- обработка ошибки в зависимости от типа и значения ошибки.

В любом случае, в этом механизме можно выделить два плюса: явность - явно видно, какая функция возвращает ошибку, и требуется ее явно обработать в месте использования. Второе - гибкость, можно обьявлять свои типы ошибок, кидать паники/забить на обработку ошибок, а можно их гранулярно хендлить и выводить, например, необходимый код HTTP для ответа в хендлере.

No OOP, RAII replaced with defer

Проблема менджмента ресурсов решается в разных языках по-разному, но это так или иначе включает целую часть языка. В Go это решается достаточно элегантно, с помощью ключевого слова defer. Оно откладывает выполнение указанной функции до момента возврата из текущей функции. Так вы можете отложить до конца функции, где бы оно не произошло, закрытие файла, анлок мьютекса, коммит транзакции и т.д. и т.п. и при этом не утечь никакие ресурсы.

Отсутствует OOP в классическом понимании, что для меня - точно плюс, т.к. OOP только запутывает код лапшой из виртуальных вызовов вдоль иерархии наследования вверх и вниз. Вместо этого предлагается использовать структуры и методы над типами, что по сути является сахаром для self.f(a, b) вместо f(self, a, b). Для случаев, когда нужно поведение, определяемое в рантайме - есть указатели на функции и интерфейсы. Для расширения функционала предлагается использовать композицию - тип A вкладывается в тип B и тип B может добавлять новые методы. Принцип подстановки при этом не соблюдается, т.к. B просто содержит A как элемент, а не использует наследование. При правильном использовании эти подходы значительно упрощают код и его поддержку.

Concurrency model

Что, как по мне, переоцененная фича, но тем не менее - каналы и горутины как легковесная замена тредам и примитивам синхронизации над общей памятью. Мьютексы все еще есть и ими можно пользоваться, но так же есть и каналы, которые позволяют более естественно выразить некоторые асинхронные операции: стриминг, асинхронное значение, fan-in, fan-out. Сам запуск параллельных потоков исполнения делается неожиданно с помощью ключевого слова go и позволяет создавать условно неограниченное число так называемых горутин. Горутины в отличие от тредов шедулятся рантаймом, а значит их имеет смысл спавнить на каждый чих, например запрос к http-серверу, при это планировщик не задохнется, а оперативная память не закончится.

Wide adoption

Ну и наконец, если вы все еще не убеждены, что Go это что-то жизнеспособное, то его используют множество компаний по всему миру, в том числе создатель в лице Google. На Go известно много созданных инфраструктурных инструментов, таких как Docker, Kubernetes, Prometheus, etc. Не менее хорошо, Go подходит для CLI приложений, например Github CLI, Terraform, etc. Также Go широко используется для написания CRUD-ов, также известных как сервисы или микросервисы. В общем, возможных применений - масса, и Go уже доказал, что с его помощью можно создавать поддерживаемые, стабильные, качественные продукты любого масштаба.