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

Комментарии (13)


  1. Dair_Targ
    23.07.2024 11:48
    +4

    Этот ваш "try, запоминающий цепочку вызовов" давно придумали, монады называются.

    Вообще вариантов, как сделать

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

    придумали примерно два: либо через исключения, либо через монады. Они, конечно, стектрейс сами-по-себе не запоминают, но всегда можно сделать специальный вариант Either, у которого Left будет хранить хоть стектрейс, хоть вообще весь контекст.


    1. GospodinKolhoznik
      23.07.2024 11:48
      +2

      Ещё можно через алгебраические эффекты.


  1. vadimr
    23.07.2024 11:48

    Стектрейс накладывает значительные ограничения на оптимизацию кода компилятором.


  1. askharitonov
    23.07.2024 11:48

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


  1. Free_ze
    23.07.2024 11:48
    +3

    Например, ключевое слово try перед вызовом функции

    1 в 1 путь Rust с его макросом try!. Тогда сразу стоит сразу внедрять оператор ?, чтобы избежать конфликтов имен и избавиться от ада скобочек:
    try(try(try(foo()).bar()).baz()) -> foo()?.bar()?.baz()?

    ЗЫ дизайн-доки для закрытой дискуссии нагляднее.


  1. qiper
    23.07.2024 11:48
    +1

    if err != nil {

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

    }

    Или хотя бы позволяли писать как-то так:

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


  1. jonic
    23.07.2024 11:48

    Мне кажется что как бы функция прочитать из базы и должна говорить что именно пошло не так, не?

    Почему нельзя сделать контекст вызова, который передавать внутрь функций и его же получать обратно? Если контекст стал в ошибку все следующие функции прокатываются впустую, наша функция где мы это делаем возвращает в итоге контекст с конкретной ошибкой. Как паразитное явление в контекст можно еще и результат записывать и тут же предавать его дальше по цепочке. А еще мокать контекст можно. или сериализовывать. Блин я похоже придумал как мне может понравится го.


    1. ponikrf
      23.07.2024 11:48

      Тоже гемор какой то. Вобще я согласен отчасти с автором статьи, но отчасти нет. Тут 2 стула - как не крути. Либо мы добавляем абстракции в виде сахара или паттернов. Либо мы оставляем простоту, но пишем больше нудятины.


      1. jonic
        23.07.2024 11:48

        Ну не знаю, было

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

        Стало

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


    1. 9982th
      23.07.2024 11:48

      Кажется вы изобрели вышеупомянутый Either.


      1. jonic
        23.07.2024 11:48

        Это отлично, учитывая что по go у меня только книжка есть и небольшое желание его изучать)

        Хотя глянул и это не совсем то


  1. Biblusha
    23.07.2024 11:48

    1. varanio Автор
      23.07.2024 11:48

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