Команда 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. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Комментарии (7)
DwarfMason
01.10.2025 11:16Тут либо я совсем уж тупой, либо лыжи не едут.
Если вы угадали
[hello world !]
, то вы знаете больше, чем кто-либо должен знать о причудах дурацкого языка программирования.Мы создали слайс 0:0 по массиву и отправили его в функцию. В функции мы решили при помощи append растянуть массив. Append по слайсу, Карл! Потом удивляемся а почему не отработало.
Go не удовлетворился одной ошибкой на миллиард долларов, поэтому они решили иметь два варианта
NULL
.nil как отсутствие ссылки и nil как отсутствие значения, сначала люди ноют, что документация в каком-либо продукте плохая, а затем сами её не читают.
Постойте, что? Почему
err
используется повторно дляfoo2()
?Берем и именуем внешние и внутренние поля скоупов по разному и получаем однозначное предсказуемое поведение.
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 }
mapcuk
Всё так! Но есть ли такой язык, в котором всё хорошо и ничего не бесит и со статической типизацией? Подскажите такой язык, буду прогать на нём просто для души.
JuPk
Может, этот?
https://www.roc-lang.org/
sqooph
Purebasic)))