Довольно много вещей, которые я думаю. Но это не пост ненавистников Go. Наоборот, на самом деле. Последние 1,5 года большую часть кода я писал на Go. Не потому, что я должен, а потому, что я так решил. Дизайнеры Go многое сделали правильно, поэтому этот язык мне очень нравится. И когда я увидел, что они не используют исключения, я был очень взволнован, потому что я думаю, что исключения изначально ошибочны.

Но вот в чем дело: Меня не устраивает обработка ошибок в Go.

Я аплодирую создателям Go за то, что они не создали еще один язык с исключениями, поскольку исключения приводят к некоторым ужасным практикам сами по себе.

Однако решение, реализованное в настоящее время в Go, также не решает проблему. Комментарий Роба Пайка:

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

звучит броско и мощно, но обработка ошибок в Go имеет несколько проблем, которые становятся очевидными только на практике. Давайте посмотрим, что это такое:

Забыть обработать ошибку (I)

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

reader := ...
file, e := os.Open(...)
if e != nil {
    // handle error
}
defer file.Close()
_, e = io.Copy(file, reader)

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

Повторное использование e здесь вообще опасно, и это противоречит всем хорошим урокам, которые мы извлекли из функциональных языков и использования неизменяемых значений.

Но и придумывание новых имен переменных для каждой ошибки в функции, чтобы избежать переопределения значения, тоже не поможет (помимо ухудшения читабельности). В идеале ошибка находится в области действия только во время проверки, как при назначении ее как части конструкции if:

if file, e := os.Open(...); e != nil {
    // handle error
}

Но и это не идеально, потому что теперь мы не можем использовать file после блока if. Объявление его явно перед блоком if тоже кажется неуклюжим и многословным.

Забыть обработать ошибку (II)

Приведенный выше пример на самом деле показывает еще одну проблему возвращаемых значений: когда вас не интересует возвращаемое значение, например, в функции копирования выше (количество записанных байтов) или для функций, которые просто возвращают один значение, компилятор не предупредит вас, если вы забудете присвоить возвращаемое значение локальной переменной.

Я думаю, что если аргумент состоит в том, что для множественных возвращаемых значений компилятор не разрешает неиспользуемые локальные переменные, то было бы только следствием требовать всегда присваивать возвращаемые значения переменным. Чтобы игнорировать ошибку, можно использовать _, как это обычно бывает при использовании нескольких возвращаемых значений.

Проверка ошибок

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

При использовании незнакомых API это может быть довольно сложно сделать правильно. Кроме того, он может легко сломаться с новой версией библиотеки, не заметив этого. Здесь мог бы помочь механизм, принудительно применяемый компилятором (например, ловушка в стране исключений).

Трассировки стека и цепочки причин ошибок

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

open /tmp/shna7fknd.conf: no such file or directory

мало помогает. Но что-то вроде

open /tmp/shna7fknd.conf: no such file or directory
Loading config failed
main.LoadConfig
 /Users/pego/.../main.go:12
main.main
 /Users/pego/.../main.go:16

сразу поправимо.

Хорошая новость в том, что для него есть библиотека:



с полезным сообщением в блоге об этом:



Тем не менее, я немного озадачен тем, что уроки, извлеченные из языков, использующих исключения, не были учтены. Эта концепция ортогональна дебатам об исключениях и возвращаемых значениях.

Ошибки в тестах или быстрый сбой

В настоящее время нет простого способа превратить возвращенную ошибку в панику. Всегда есть как минимум этот шаблон:

e := someFunc()
if e != nil {
    panic(e)
}

Это раздражает в тестах или коде начальной загрузки, который должен быстро дать сбой, если что-то пойдет не так.

Можно объединить три строки в одну, извлекая ее. Тем не менее, он оставляет шум в коде, который на самом деле не заботится об ошибках, кроме «сбой, если что-то пойдет не так».

Я не говорю, что эти проблемы нельзя обойти или свести к минимуму тщательным написанием кода. Но правда в том, что при всех благих намерениях и так далее код никогда не бывает идеальным. Если бы язык мог помочь избежать некоторых из этих упущений (даже если это означает некоторую сложность языка), это было бы большим преимуществом.

Что все это значит? Ну, очевидно, в версии 1 язык не изменится. Но я думаю, что улучшение обработки ошибок следует учитывать при обсуждении версии 2.

Выделенные структуры управления обработкой ошибок могли бы упростить это и сделать более безопасным, не добавляя уродства и многословия традиционных конструкций try-catch.