На нынешней работе приходится использовать Go. Я хорошо познакомился с этим языком. Мне он не нравится, и меня озадачивает его популярность.
Эргономика разработки
Никогда не встречал языка, настолько открыто противостоящего удобству для разработчика. К примеру, Роб Пайк неоднократно и открыто враждебно относился к любому обсуждению подсветки синтаксиса на Go Playground. В ответ на разумно сформулированные вопросы пользователей его публичные ответы отсвечивали пренебрежением и неуважением:
Gofmt написан специально чтобы уменьшить количество бессмысленных дискуссий о форматировании кода, что отлично удалось. К сожалению, это никак не повлияло на количество бессмысленных дискуссий о подсветке синтаксиса или, как я предпочитаю её называть, spitzensparken blinkelichtzen.
И снова в ветке 2012 Go-Nuts:
Подсветка синтаксиса — для маленьких. В детстве меня учили арифметике на цветных палочках. Сейчас я вырос и использую чёрно-белые цифры.
Ясно, что из знакомых Роба никто не страдает от синестезии, дислексии или плохого зрения. Из-за его позиции официальный сайт и документация Go до сих пор остаются без подсветки синтаксиса.
Группа разработчиков Go не ограничивается Пайком, но остальные всячески поддерживают его отношение к эргономике. В обсуждении типов union/sum пользователь ianlancetaylor отклонил запрос, конкретно определяющий преимущество эргономики, как слишком незначительный и не достойный внимания:
Это обсуждалось несколько раз в прошлом, в том числе до открытого релиза. Тогда мы пришли к мнению, что типы sum не особо расширяют интерфейсные типы. Если разобраться, в итоге всё сводится к тому, что компилятор проверяет, что вы заполнили все случаи переключения типов. Это довольно небольшое преимущество, чтобы менять язык.
Такое отношение расходится с мнением о типах union в других языках. В 2000 году JWZ критиковал Java:
Также я думаю, что для моделирования enum и :keywords используются довольно ламерские идиомы. (Например, компилятор не имеет возможности выдать спасительное предупреждение, что «
`enumeration value x'
, не обработано в switch»).
Команда Java приняла такую критику близко к сердцу, и теперь Java может выдать это предупреждение для операторов множественного выбора по типам перечисления. Другие языки — в том числе и современные языки, такие как Rust, Scala, Elixir и им подобные, а также собственный прямой предок Go, язык C — тоже выдают предупреждения, где это возможно. Очевидно, что такие предупреждения полезны, но для команды Go комфорт разработчика недостаточно важен и не заслуживает рассмотрения.
Политика
Нет, я не про интриги в списках рассылках и на встречах. Вопрос более глубокий и интересный.
Как и любой язык, Go является инструментом политики. Он воплощает определённый набор представлений, как должно быть написано и организовано программное обеспечение. В случае Go воплощается чрезвычайно жёсткая кастовая иерархия «опытных программистов» и «неквалифицированных программистов», навязанная самим языком.
Со стороны неквалифицированных программистов язык запрещает функции, которые считаются «слишком продвинутыми». Здесь нет универсальных дженериков, нельзя писать функции более высокого порядка, которые обобщают более чем один конкретный тип, и чрезвычайно строгие правила о наличии запятых, неиспользуемых символов и других недостатках, которые могут возникнуть в обычном коде. Программисты Go живут в мире, который ещё более ограниченный, чем Java 1.4.
Опытным программистам доверяют эти функции — и они могут отдавать системы на таком коде коллегам по обе стороны барьера. Реализация языка содержит родовые функции, которые нельзя использовать на практике, и с отношениями типов, которые язык просто не способен выразить. Это мир, в котором живут разработчики программ на Go.
Не знаю как внутри Google, но за её пределами это негласное политическое разделение программистов на «благонадежных» и «неблагонадёжных» лежит в основе многих рассуждений о языке.
Пакеты и распространение кода
Пакетный менеджер go get
разочаровывает своим отказом от ответственности. Границы пакетов — это место для коммуникации между разработчиками, а команда Go принципиально отказывается помочь.
Я могу уважать позицию команды Go, которая заключается в том, что это не их проблема, но тут их действия невыгодно отличаются от других основных языков. Достаточно вспомнить катастрофическую историю попыток управления пакетами для библиотек C и посмотреть на Autotools — пример того, как долго может сохраняться столь бедственное положение. С учётом этого весьма удивительно наблюдать, что команда разработчиков языка в 21 веке умывает руки в такой ситуации.
GOPATH
Монолитный путь для всех исходников неизбежно приводит к конфликтам версий между зависимостями. Настройка vendor
частично решает эту проблему за счёт существенного раздутия репозитория и нетривиального изменения связей, которые могут привести к ошибкам, если в одном приложении остались ссылки на «вендорскую» и «невендорскую» копии одной и той же библиотеки.
Опять же, ответ команды Go «не наша проблема» разочаровывает и расстраивает.
Обработка ошибок в Go
Стандартный подход Go к действиям, которые могут завершиться ошибкой, включает в себя возврат нескольких значений (не многокомпонентного объекта; в Go таких нет) с типом последнего значения error
в виде интерфейса, где значение nil
означает «ошибки нет».
Поскольку это негласное соглашение, оно не представлено в системе типов Go. Не существует обобщённого типа, представляющего результат потенциально ошибочной операции, над которым можно писать полезные объединяющие функции. Более того, он не всегда соблюдается: ничто, кроме здравого смысла, не мешает программисту вернуть error
в каком-то ином виде, например, в середине последовательности возвращаемых значений или в начале — поэтому методы обработки ошибок тоже чреваты проблемами.
В Go невозможно составить потенциально ошибочные операции более лаконичным способом, чем что-то такое:
a, err := fallibleOperationA()
if err != nil {
return nil, err
}
b, err := fallibleOperationB(a)
if err != nil {
return nil, err
}
return b, nil
В других языках это можно сформулировать как
a = fallibleOperationA()
b = fallibleOperationB(a)
return b
в языках с исключениями или
return fallibleOperationA()
.then(a => fallibleOperationB(a))
.result()
в языках с соответствующими абстракциями.
Это существенная разница, особенно если у вас длинные последовательности таких операций (даже при поддержке редактора, который поддерживает генерирацию ветвей). Приходится тратить лишнее время на кодирование и дополнительные когнитивные усилия на чтение кода. Руководства по стилю помогают, но смешивание стилей только усугубляет ситуацию. Как пример:
a, err := fallibleOperationA()
if err != nil {
return nil, err
}
if err := fallibleOperationB(a); err != nil {
return nil, err
}
c, err := fallibleOperationC(a)
if err != nil {
return nil, err
}
fallibleOperationD(a, c)
return fallibleOperationE()
Да поможет вам Бог сделать вложение или что-то кроме передачи ошибки обратно на стек.
Автор: m1rko