Ошибки в языке Go — это большая ошибка

b6c44e01dc9649d6af264582698c03ab
// гофер пытается найти логику среди обработки ошибок
+-------+-------+-------+-------+-------+-------+
|       |  err  |       |  err  |       |  err  |
|  ,_,,,        |       |       |       |       |
| (◉ _ ◉)       |       |       |       |       |
|  /)  (\               |       |       |       |
|  ""  ""               |       |       |       |
+       +-------+       +-------+       +-------+
|       |  err          |  err  |       |  err  |
|       |               |       |       |       |
|       |               |       |       |       |
+-------+       +-------+       +-------+       +
|  err  |               |  err                  |
|       |               |                       |
|       |               |                       |
+       +-------+       +       +-------+       +
|       |  err  |               |  err  | logic |
|       |       |               |       |       |
|       |       |               |       |       |
+-------+-------+-------+-------+-------+-------+

Я пишу на Go несколько лет, в Каруне многие вещи сделаны на нём; язык мне нравится своей простотой, незамысловатой прямолинейностью и приличной эффективностью. На других языках я писать не хочу.

Но сорян, к бесконечным if err != nil я до конца привыкнуть так и не смог.

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


Читабельность

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

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

Возьмем самый-самый простой пример (псевдокод):

Вместо такого

прочитали из базы()
обработали()
записали результат()

Мы имеем

прочитали из базы()
if err != nil {
    return fmt.Errorf("не смогли прочитать из базы: %w", err)
}
обработали()
if err != nil {
   return fmt.Errorf("не смогли обработать: %w", err)
}
записали результат()
if err != nil {   
   return fmt.Errorf("не смогли записать результат: %w", err)
}

Какой из двух кусков кода боле читабелен?

Да, второй более explicit, и да, он досконально проверяет всю ошибочную фигню, но зато в первом примере сразу понятно, что происходит, а во втором приходится продираться через мусор, потому что сама логика происходящего занимает намного меньше площади экрана, чем ошибочные реакции. А если сама логика сложна и содержит какие-то хитроумные условия и подсчёты? Там капец просто.

И это, знаете ли, big fucking deal. Как вы, наверно, знаете, при программировании люди тратят на чтение кода 80–90% времени, а на написание совсем чуть-чуть. Т.е. сначала надо разобраться, что уже происходит, и лишь потом добавлять новое. Так вот, с чтением кода в Go совсем беда. И беда эта связана только с обработкой ошибок, всё остальное — в пределах нормы.


Стек трейс

Стандартный пакет errors не сохраняет стек вызовов, поэтому, когда вы в конце концов на самом высоком уровне получили ошибку и записали её в лог, в логе вы просто так не поймёте, где изначально была проблема. Например, ошибка была «ошибка SQL запроса». Где sql, какой именно запрос из сотен? А трейс есть только на момент записи лога, остальной стек уже потерян. Именно поэтому люди вынуждены выкручиваться: использовать сторонние пакеты или прояснять ошибку вручную, добавляя информацию на каждом слое (через fmt.Errorf) или логировать прямо в месте ошибки (ещё больше захламляя логику).

В общем, на практике вместо хотя бы

if err != nil {
   return nil, err
}

чаще всего идёт оборачивание

if err != nil {
   return nil, fmt.Errorf("мы тут делали то-то и то-то, а нам вернули ошибку: %w", err)
}

Причём это объяснение в 99% нужно не для лучшего понимания происходящего, а тупо для того, чтобы потом по сообщению в логе найти место возникшей проблемы.


Что делать?

Есть несколько десятков proposals, которые предлагают разные способы упрощения языка.

Например, ключевое слово try перед вызовом функции, которое работает практически как макрос, неявно добавляющий if err!= nil {return nil, err}.

или так:
callSomeFunction() orfail (см здесь)

Тысячи их. Как по мне, надо сделать этот самый try. Но его недостаточно, потому что никто не возвращает просто ошибку, её возвращают с пояснениями. Поэтому нужен try, запоминающий цепочку вызовов. И вот это уже будет бомба.

Статья написана по мотивам поста из канала Cross Join.

© Habrahabr.ru