- PVSM.RU - https://www.pvsm.ru -

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

// гофер пытается найти логику среди обработки ошибок
+-------+-------+-------+-------+-------+-------+
|       |  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 [1], которые предлагают разные способы упрощения языка.

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

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

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

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

Автор: Антон Околелов

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/errors/392884

Ссылки в тексте:

[1] несколько десятков proposals: https://github.com/golang/go/issues?q=is%3Aopen+label%3Aerror-handling++label%3Aproposal

[2] ключевое слово try: https://github.com/golang/go/issues/32437

[3] здесь: https://github.com/golang/go/issues/67955

[4] Cross Join: https://t.me/crossjoin

[5] Источник: https://habr.com/ru/companies/karuna/articles/830346/?utm_source=habrahabr&utm_medium=rss&utm_campaign=830346