Команда Go for Devs подготовила перевод статьи о том, почему спустя десять лет автор по-прежнему критикует Go. Ошибки на миллиард долларов, загадочный nil, проблемы с памятью и «магия» defer — по мнению автора, всё это делает язык излишне сложным и болезненным. А стоит ли оно того?
Область видимости переменной ошибки
Вот пример того, как язык заставляет вас делать неправильно. Для читателя кода (а код читают чаще, чем пишут) очень полезно минимизировать область видимости переменной. Если с помощью простого синтаксиса вы можете сообщить читателю, что переменная используется только в этих двух строках, это хорошо.
Пример:
if err := foo(); err != nil {
return err
}
(Уже достаточно сказано об этом многословном повторяющемся бойлерплейте, так что мне не нужно. И мне это особо не интересно.)
Так что это нормально. Читатель знает, что err существует здесь и только здесь.
Но затем вы сталкиваетесь с этим:
bar, err := foo()
if err != nil {
return err
}
if err = foo2(); err != nil {
return err
}
[… a lot of code below …]
Постойте, что? Почему err используется повторно для foo2()? Может быть, есть что-то тонкое, чего я не замечаю? Даже если мы изменим это на :=, нам остаётся гадать, почему err находится в области видимости (потенциально) для остальной части функции. Почему? Будет ли она прочитана позже?
Особенно при поиске ошибок опытный программист увидит эти вещи и замедлится, потому что здесь что-то нечисто. Хорошо, теперь я потратил пару секунд на ложный след повторного использования err для foo2().
Может быть, ошибка в том, что функция заканчивается так?
// Возвращаем foo99() error. (упс, но на самом деле мы делаем не это)
foo99()
return err // Это `err` из самого верха, из вызова foo().
Почему область видимости err простирается далеко за пределы того, где она актуальна?
Код был бы намного легче читать, если бы область видимости err была меньше. Но это синтаксически невозможно в Go.
Это было непродуманно. Принятие такого решения было не обдумыванием, а набором текста.
Два типа nil
Посмотрите на этот абсурд:
package main
import "fmt"
type I interface{}
type S struct{}
func main() {
var i I
var s *S
fmt.Println(s, i) // nil nil
fmt.Println(s == nil, i == nil, s == i) // t,t,f: Они равны, но на самом деле — нет.
i = s
fmt.Println(s, i) // nil nil
fmt.Println(s == nil, i == nil, s == i) // t,f,t: Они не равны, но при этом — равны.
}
Go не удовлетворился одной ошибкой на миллиард долларов, поэтому они решили иметь два варианта NULL.
«А какого цвета ваш nil?» — Ошибка на два миллиарда долларов)
Причина различия сводится опять же к отсутствию размышлений, простому набору текста.
Это не переносимо
Добавление комментария в верхней части файла для условной компиляции — это, должно быть, самая глупая вещь на свете. Любой, кто на самом деле пытался поддерживать переносимую программу, скажет вам, что это приведёт только к страданиям.
Это Аристотелевский подход к науке создания языков: запереться в комнате и никогда не проверять свои гипотезы на практике.
Проблема в том, что сейчас не 350 год до нашей эры. У нас уже есть опыт, показывающий, что, помимо сопротивления воздуха, тяжелые и легкие объекты падают с одинаковой скоростью. И у нас есть опыт работы с переносимыми программами, и мы бы не стали делать что-то настолько глупое.
Если бы это был 350 год до нашей эры, это можно было бы простить. Наука в том виде, в каком мы ее знаем, еще не была изобретена. Но это происходит после десятилетий широко доступного опыта в области переносимости.
Подробнее в этом посте.
append без определенного владения
Что этот код выведет?
package main
import "fmt"
func foo(a []string) {
a = append(a, "NIGHTMARE")
}
func main() {
a := []string{"hello", "world", "!"}
foo(a[:1])
fmt.Println(a)
}
Вероятно, [hello NIGHTMARE !]. Кому это нужно? Никому это не нужно.
Хорошо, а как насчет этого?
package main
import "fmt"
func foo(a []string) {
a = append(a, "BACON", "THIS", "SHOULD", "WORK")
}
func main() {
a := []string{"hello", "world", "!"}
foo(a[:1])
fmt.Println(a)
}
Если вы угадали [hello world !], то вы знаете больше, чем кто-либо должен знать о причудах дурацкого языка программирования.
defer — это глупо
Даже в языках со сборщиком мусора иногда просто нельзя ждать уничтожения ресурса. Действительно необходимо, чтобы он был выполнен при выходе из локального кода, будь то обычный возврат или исключение (известное как паника).
Мы явно хотим RAII или что-то похожее.
В Java это есть:
try (MyResource r = new MyResource()) {
/*
работаем с ресурсом r, который будет освобождён по завершении области видимости
через .close(), а не тогда, когда захочет GC.
*/
}
В Python это есть. Хотя Python почти полностью основан на подсчете ссылок, поэтому можно в значительной степени полагаться на вызов финализатора __del__ . Но если это важно, то есть синтаксис with.
with MyResource() as res:
# какой-то код. В конце блока у res будет вызван __exit__.
Go? Go заставляет вас читать руководство и выяснять, нужно ли вызывать defer-функцию для этого конкретного ресурса и какую именно.
foo, err := myResource()
if err != nil {
return err
}
defer foo.Close()
Это так глупо. Некоторые ресурсы требуют отложенного уничтожения. Некоторые нет. Какие именно? Удачи, блин.
И вы также регулярно сталкиваетесь с такими чудовищами:
f, err := openFile()
if err != nil {
return nil, err
}
defer f.Close()
if err := f.Write(something()); err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
Да, это то, что вам НУЖНО сделать, чтобы безопасно записать что-то в файл в Go.
Что это, второй Close()? О да, конечно, это нужно. Безопасно ли вообще закрывать дважды, или мой defer должен это проверять? На os.File это безопасно, но на других вещах: КТО ЗНАЕТ?!
Стандартная библиотека поглощает исключения, так что вся надежда потеряна
В Go утверждают, что в языке нет исключений. Go делает использование исключений крайне неудобным, потому что разработчики языка хотят «наказывать» программистов, которые их используют.
Хорошо, пока что так.
Но все Go-программисты всё равно должны писать отказоустойчивый код. Потому что, хотя они и не используют исключения, другой код будет их использовать. Будут паники (panic).
Поэтому вам нужно, не просто следует, а НЕОБХОДИМО писать код вроде:
func (f *Foo) foo() {
f.mutex.Lock()
defer f.mutex.Unlock()
f.bar()
}
Что это за дурацкая система с «перевёрнутым» порядком? Это так же глупо, как ставить день посередине даты. ММДДГГ, серьёзно?
Но ведь паника завершит программу, скажут они, так почему вы должны беспокоиться о разблокировке мьютекса за пять миллисекунд до её завершения?
А что, если что-то «проглотит» это исключение и продолжит работу как ни в чем не бывало, а вы останетесь с заблокированным мьютексом?
Но ведь никто так не поступит, верно? Разумные и строгие стандарты кодирования наверняка предотвратят это под угрозой увольнения?
Стандартная библиотека делает это. fmt.Print при вызове .String(), и стандартный HTTP-сервер тоже так поступает с исключениями в HTTP-обработчиках.
Вся надежда потеряна. Вы ОБЯЗАНЫ писать отказоустойчивый код. Но вы не можете использовать исключения. Вам остаются только недостатки исключений, которые обрушиваются на вас.
Не позволяйте им вводить вас в заблуждение.
Иногда данные не являются UTF-8
Если вы помещаете случайные бинарные данные в string, Go просто продолжает работать, как описано в этом посте.
За десятилетия я терял данные из-за того, что инструменты пропускали файлы с названиями не в UTF-8. Меня не следует винить за наличие файлов, названных до появления UTF-8.
Ну… они у меня были. Теперь их нет. Они были тихо пропущены при резервном копировании/восстановлении.
Go хочет, чтобы вы продолжали терять данные. Или, по крайней мере, когда вы потеряете данные, он скажет: «Ну, в какую кодировку были обёрнуты данные?».
Или, может быть, вы просто сделаете что-то более продуманное, когда будете проектировать язык? Как насчёт того, чтобы сделать правильную вещь, вместо очевидно неправильной простой вещи?
Использование памяти
Почему меня волнует использование памяти? Оперативная память дешевая. Гораздо дешевле, чем время, которое требуется, чтобы прочитать этот пост в блоге. Меня волнует, потому что мой сервис работает на облачном экземпляре, где вы на самом деле платите за оперативную память. Или вы запускаете контейнеры и хотите запустить тысячу из них на одной машине. Ваши данные могут поместиться в оперативной памяти, но это всё равно дорого, если вам приходится выделять тысяче контейнеров 4 ТБ оперативной памяти вместо 1 ТБ.
Вы можете вручную запустить сборщик мусора с помощью runtime.GC(), но «о нет, не делайте этого», говорят они, «он запустится, когда это потребуется, просто доверьтесь ему».
Да, в 90% случаев это работает всегда. Но потом перестаёт.
Я переписал кое-что на другом языке, потому что со временем версия на Go потребляла всё больше и больше памяти.
Так не должно было быть
Мы знали лучше. Это не был спор о COBOL, использовать ли символы или английские слова.
И это не похоже на то время, когда мы не знали, что идеи Java были плохими, потому что мы знали, что идеи Go были плохими.
Мы уже знали, что Go не идеален, и всё же теперь мы застряли с плохими кодовыми базами на Go.
Русскоязычное Go сообщество

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Комментарии (21)

DwarfMason
01.10.2025 11:16Тут либо я совсем уж тупой, либо лыжи не едут.
Если вы угадали
[hello world !], то вы знаете больше, чем кто-либо должен знать о причудах дурацкого языка программирования.Мы создали слайс 0:0 по массиву и отправили его в функцию. В функции мы решили при помощи append растянуть массив. Append по слайсу, Карл! Потом удивляемся а почему не отработало.
Go не удовлетворился одной ошибкой на миллиард долларов, поэтому они решили иметь два варианта
NULL.nil как отсутствие ссылки и nil как отсутствие значения, сначала люди ноют, что документация в каком-либо продукте плохая, а затем сами её не читают.
Постойте, что? Почему
errиспользуется повторно дляfoo2()?Берем и именуем внешние и внутренние поля скоупов по разному и получаем однозначное предсказуемое поведение.

pda0
01.10.2025 11:16Потом удивляемся а почему не отработало.
Оно сработало. Просто неожиданно. :)
Тут либо я совсем уж тупой, либо лыжи не едут.
Лыжи не едут. Срезы в go устроены так: структура (ссылка на буфер, размер, вместимость). Пока кол-во вставляемых элементов не превышает вместимость среза - они просто дописываются в буфер и увеличивается размер. Если превышено - увеличивается вместимость, выделяется новый буфер, память копируется в него, переключаемся на новый буфер. И далее как выше. Вместимость кратна степени двойки.
Первый случай:
a - срез с размером 3, вместимостью 4.
Передаём в функцию срез с него с тем же буфером, размером 1, вместимостью 4.
Добавляем один элемент. Это не превышает вместимость, буфер остаётся прежним,
NIGHTMAREпишется в буфер, переписывая world.Второй случай.
Добавляем 4 элемента, это превышает вместимость - размер, по этому в функции буфер среза копируется, отвязывая его от буфера исходного среза и добавляет в него элементы. Но буфер исходного среза не затронут.
По этому я и написал в первой статье тот каммент.

poiqwe
01.10.2025 11:16С самого начала же проговаривается, что slice это структура данных только указывающая на массив. Очевидно, что и при копировании скопируется только slice, а не массив на который он указывает.
Насчет defer, кажется можно сделать такf, err := openFile() if err != nil { return nil, err } defer function() { if cerr := f.Close(); cerr != nil && err == nill { err = cerr } }() if err := f.Write(something()); err != nil { return nil, err }

sqooph
01.10.2025 11:16– Бэрримор, кто снова воет на болотах!?
– Гоферы, сэр....

koreychenko
01.10.2025 11:16Дык это не гоферы воют, а джависты, которые пришли в го с какими-то своими представлениями и не читали документацию и best practice. Плюс набрали всяких синтетических примеров типа как на собеседованиях задают. У нормальных гоферов все в порядке, потому что код как в статье никто в реале не пишет.
P.S. странно, что автор не докопался до контекстов и горутин. Вот где пострадать можно! А то давайте в 100500 раз поноем про область видимости ошибки в го - явно, что автор сам на языке не писал нормально :-)

diafour
01.10.2025 11:16Код был бы намного легче читать, если бы область видимости
errбыла меньше. Но это синтаксически невозможно в Go.Почему же синтаксически невозможно? Можно обработку по блокам раскидать:
var bar Type1 { var err error bar, err = foo() if err != nil { return err } } { if err := foo2(); err != nil { return err } }
diderevyagin
01.10.2025 11:16Человек не выучил и не разобрался в Golang. И начинает писать на нем как привык в XYZ. Вот и результат - поток чуши. Нет бы покритиковать GC к примеру (есть что критиковать).

koreychenko
01.10.2025 11:16Об том и речь, что у тех, кто написал на языке больше одного проекта, совершенно другие боли.
Это автор явно не трогал горутины, каналы и контекст. Вот там вот дофига не очевидного.

diderevyagin
01.10.2025 11:16Тут есть еще такая ловушка, как результат позиционирования "Golang простой и ничего там такого нет".
И многие потому решают что документацию читать - не надо. понимать как работает Runtime и компоненты - не надо. Сдедить за релизами и изменением поведения - не надо.
а ведь это нет так - нюансов много и разбираться надо без дураков

koreychenko
01.10.2025 11:16На самом деле все ещё интереснее.
Го простой. Но только по синтаксису.
В го можно писать без многопоточности, каналов, синхронизаций и прочей магии просто тупо в лоб как на PHP, но тогда в чем смысл его брать?
Асинхронности и её синхронизация это тема в принципе непростая и здравый смысл здесь часто работает не очевидно :-)
И это я ещё про оптимизации по памяти ничего не писал.

artalex
01.10.2025 11:16Было же уже тут не так давно https://habr.com/ru/companies/ruvds/articles/941106/

diderevyagin
01.10.2025 11:16Человек не удосужился выучить Golang и требует, чтобы в нем было то, к чему он привык в условной Java. Особенно "умилили" претензии к defer который назван "глупым"

manyakRus
01.10.2025 11:16Всё правильно написано.
Не надо привыкать к глюкам, надо чтоб глюков не было.
Но это всё равно мелкие недостатки, не страшно
mapcuk
Всё так! Но есть ли такой язык, в котором всё хорошо и ничего не бесит и со статической типизацией? Подскажите такой язык, буду прогать на нём просто для души.
JuPk
Может, этот?
https://www.roc-lang.org/
696620657272213d206e696c
Язык хороший, но он пока в альфа-версии. Ещё неизвестно, станет ли он кому-то нужен.
sqooph
Purebasic)))
hddn
Gleam: https://gleam.run/
Про него был подкаст недавно у "Подлодки"
ahdenchik
https://dlang.org/
DigLik_228653
C#