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

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


  1. Dair_Targ
    23.07.2024 11:48
    +12

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

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

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

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


    1. GospodinKolhoznik
      23.07.2024 11:48
      +6

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


  1. vadimr
    23.07.2024 11:48
    +2

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


    1. dph
      23.07.2024 11:48

      А почему, кстати? Какие именно ограничения?


      1. vadimr
        23.07.2024 11:48
        +1

        Потому что реальное состояние стека в таком случае должно соответствовать синтаксису вызовов функций в тексте программы. Вы же ведь не хотите в стектрейсе увидеть совсем другую иерархию вызовов, чем написали в коде.

        А это означает запрет инлайна, запрет оптимизации концевой рекурсии и т.п.


        1. dph
          23.07.2024 11:48
          +1

          Хм, вроде бы Scala умеет оптимизировать хвостовую рекурсию в JIT, хотя и exception там есть. В Koltin тоже есть tailrec. Да и не так часто встречается хвостовая рекурсия.
          Inlining в JIT тоже есть и работает.


          1. vadimr
            23.07.2024 11:48
            +1

            Эксцепшенам самим по себе оптимизация не мешает, так как является эквивалентным преобразованием кода. Мешает именно интроспективному просмотру стектрейса.

            Например, в C++ есть эксцепшены и де-факто есть оптимизация концевой рекурсии, но нет стектрейса. В Питоне есть эксцепшены и стектрейс, но из-за этого нет оптимизации концевой рекурсии.


            1. dph
              23.07.2024 11:48
              +1

              Так stacktrace есть во всех указанных вариантах, и в Scala и в Kotlin и в Java.


              1. vadimr
                23.07.2024 11:48

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


                1. dph
                  23.07.2024 11:48
                  +1

                  Хм, а по сравнению с чем проблемы с оптимизацией в JIT?


                  1. vadimr
                    23.07.2024 11:48

                    По сравнению с C++. Например, первая же глубокая рекурсия вашу джаву обрушит. Доходит до того, что у программистов создаётся представление о неэффективности рекурсии самой по себе, хотя неэффективен просто механизм вызова функций в некоторых языках.


                    1. dph
                      23.07.2024 11:48
                      +1

                      Ну, в kotlin добавили tailrec для этой задачи. Но глубокая рекурсия и в C++ штука опасная и требует аккуратности.
                      Но тут, вроде бы, говорили про Go, а не про C++.


                      1. vadimr
                        23.07.2024 11:48

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

                        Всё это дело индивидуальных предпочтений. Я бы лично не задумываясь предпочёл свободу рекурсии детерминированному стеку вызовов. Собственно, это основная проблема, которая меня напрягает в Питоне.


                      1. dph
                        23.07.2024 11:48
                        +1

                        Я бы предпочел возможность искажения stack trace при оптимизации рекурсии отсутствию возможности к просмотру трейса.


                      1. vadimr
                        23.07.2024 11:48

                        Именно так работает упомянутый Вами tailrec в Kotlin. Однако, если почитать полемику, то, похоже, практически никто из программистов на Kotlin не понимает точной семантики и назначения этой конструкции (отчасти в этом виновато неудачное имя, больше подошло бы что-то вроде nostackframe).

                        Однако искажение stack trace при оптимизации способно провоцировать труднообнаружимые ошибки.


            1. tzlom
              23.07.2024 11:48

              в плюсах есть стектрейсы (с++23), и концевая оптимизация начиная с С++11

              конечно трейс не покажет полную вложенность хвостовой рекурсии, однако для диагностики этого достаточно


        1. aamonster
          23.07.2024 11:48
          +1

          Положим, что касается хвостовой рекурсии – потеря N повторяющихся фрагментов в стектрейсе – не великая проблема.

          Инлайн же сам по себе особо ситуацию не портит (если, конечно, отслеживание call stack не ведётся в лоб по stack frames).

          Так что мешать оптимизации будет, но не в приведённых вами примерах :-)


          1. vadimr
            23.07.2024 11:48

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

            Допустим, с точки зрения семантики языка ничто не мешает выходить из рекурсии по росту стектрейса. Хотя я бы не стал рекомендовать такую практику.

            Мнение разработчика Питона, например, состоит в том, что именно стектрейс является основным препятствием для оптимизации концевой рекурсии.


            1. aamonster
              23.07.2024 11:48

              Я не в курсе, для чего может использоваться "автоматическая интроспекция" в данном случае (и вообще по возможности избегаю работы через рефлексию). Знаю только, что если убрать рекурсивный вызов внутрь try-catch или ещё чего, что должно обрабатывать результат вызова – это не хвостовая рекурсия :-)


              1. vadimr
                23.07.2024 11:48

                Довольно сложно представить необходимость собственного обработчика try-catch на каждом уровне рекурсии.

                А одному общему обработчику концевая рекурсия не мешает, как и любой другой способ реализации цикла.

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


                1. aamonster
                  23.07.2024 11:48

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

                  Впрочем, я вообще не люблю такую рекурсию, я люблю вместо неё fold/zip/...


                  1. vadimr
                    23.07.2024 11:48

                    Конечно, если есть возможность применить функции высших порядков, то лучше так и делать.


    1. event1
      23.07.2024 11:48

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


      1. vadimr
        23.07.2024 11:48

        Ядро линукса не гарантирует соответствия своего стектрейса синтаксису вызовов в языке программирования. Поэтому и проблемы нет.


        1. event1
          23.07.2024 11:48

          Ну так, не давайте такой гарантии в Go тоже. И проблемы тоже не будет.


  1. askharitonov
    23.07.2024 11:48

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


    1. TimsTims
      23.07.2024 11:48

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

      Вы так говорите, будто создатели Go жили 2000лет назад , и истинные цели его создания теперь окутаны библией тайной, никто точно не знает почему так было сделано, ведь спросить не у кого.


  1. Free_ze
    23.07.2024 11:48
    +10

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

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

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


  1. qiper
    23.07.2024 11:48
    +5

    if err != nil {

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

    }

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

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


    1. olivera507224
      23.07.2024 11:48

      В одну строчку? Или принципиально без фигурных скобок?


      1. UranusExplorer
        23.07.2024 11:48
        +1

        Да. Так гораздо лучше выглядит, и главное логичнее: одна строка - одна операция (обработку ошибки за отдельную операцию естественно не считаем)


        1. olivera507224
          23.07.2024 11:48
          +1

          Я сам такой же и обычно так и отвечаю, поэтому поясню вопрос :)

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


  1. jonic
    23.07.2024 11:48
    +1

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

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


    1. ponikrf
      23.07.2024 11:48
      +1

      Тоже гемор какой то. Вобще я согласен отчасти с автором статьи, но отчасти нет. Тут 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. olivera507224
          23.07.2024 11:48

          Будут ли вызваны две нижние функции в случае, если ошибка на этапе выполнения верхней?


          1. jonic
            23.07.2024 11:48

            Будут, поэтому внутри каждой функции должно быть вначале что то типа if(ctx.isFail()) { return ctx; }

            Я так понимаю можно это сделать макросом или препроцесосром, я не пишу на го, поэтому это просто полет фантазии.


            1. olivera507224
              23.07.2024 11:48
              +1

              Если бы можно было, то уже давно пресловутую конструкцию if err != nil сделали бы макросом или препроцессором.

              Будут, поэтому внутри каждой функции должно быть вначале что то типа if(ctx.isFail()) { return ctx; }

              И тогда мы получаем ту же самую конструкцию if err != nil, только заходим немного с другого конца :) Право на жизнь ваш концепт, несомненно, имеет, особенно в мире, где есть реализация обработки ошибок в Го. Но как по мне, всё же лучше прервать цепочку вызовов там, где она больше не может продолжаться, чем продолжить её выполнение в усечённом виде.


              1. jonic
                23.07.2024 11:48

                Ну она как бы прервалась, просто оверхед на вызов пустых функций. Но да, тут есть над чем подумать. Под макросом я понимал дефайн как в C, прячущий это в единую конструкцию похожую на вызов метода. Не уверен что это есть правда. Но в целом я уже целый день думаю об этом, зачем-то))


                1. olivera507224
                  23.07.2024 11:48

                  Под макросом я понимал дефайн как в C

                  А, понял. Не, Го не умеет в метапрограммирование, максимум на что он способен - это кодогенерация.

                  Но в целом я уже целый день думаю об этом, зачем-то

                  Потому что это интересно :)


                  1. jonic
                    23.07.2024 11:48

                    Но дженерики то есть? Я вроде гуглил и были?


                    1. olivera507224
                      23.07.2024 11:48

                      Дженерики есть, хоть и кастрированные. А чем они в данном контексте могут помочь?


              1. aamonster
                23.07.2024 11:48

                "Выполнение в усечённом виде" порой заметно упрощает код (если у вас clean-up не в деструкторах или что там вам язык предоставляет для его автоматизации и вы не можете на любую ошибку делать return).

                И теоретически убирание такого усечённого выполнения – довольно понятная задачка для оптимизирующего компилятора.


                1. olivera507224
                  23.07.2024 11:48

                  порой 


                  1. aamonster
                    23.07.2024 11:48
                    +3

                    Именно :-)

                    Очень наглядный пример – язык Objective C, с его "отправкой сообщения" вместо привычного "вызова метода". Наиболее заметное отличие – что можно отправить сообщение нулевому объекту, и он ничего не сделает. Порой это упрощает код (не надо делать 100500 проверок, что полученные нами объекты существует – к примеру, передали нам нулевое view, добываем из него нулевое же window и что-то с ним "делаем"), но порой изрядно затрудняет поиск ошибок (если такая логика не планировалась специально – лучше б упасть на первом же нулевом указателе), в результате в ObjC появились атрибуты __nullable и __nonnull. А в более поздних языках такие вещи вообще делаются явно, вызовом myObject?.myMethod(), что практически не загромождает код, но позволяет видеть, что ты делаешь.


    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 все называют обработку ошибок как одну из главных проблем го. Так что видимо аудитории разные у опросов


  1. johhy13
    23.07.2024 11:48

    Верятно как в Hare было бы более удобно

    https://harelang.org/tutorials/introduction#a-few-words-about-error-handling


    1. event1
      23.07.2024 11:48
      +3

      Сопоставление с образцом — слишком сложная концепция для Go


  1. NeoCode
    23.07.2024 11:48
    +2

    Кажется, и в Rust и в Swift и в Zig применяется "легковесная" схема обработки ошибок с optional и either и оператором, который возвращает управление из вызывающей функции, если вызываемая функция вернула ошибку (префиксный try или постфиксный "?"). Данная схема выглядит весьма привлекательно и кажется, ее вполне можно имплементировать в Go. Но для этого в язык совершенно точно нужно протащить опционалы и Either. И интересно, как это можно совместить с принятой в Go схемой возврата двух значений - смыслового и кода ошибки, так чтобы старые функции без переписывания заработали по новой схеме?


    1. domix32
      23.07.2024 11:48
      +3

      Опционалы в Go фактически существуют. Проблема именно с обработкой - нет удобного способа их обработки. В Rust есть варианты, что можно сделать с ошибкой - лифтануть ошибку выше при помощи элвиса (try_do()? ), запаниковать на месте (try_do().expect("can't do")), конвертнуть (.map_err()), либо вообще бесстрашно развернуть в значение (всякие unwrap(), unwrap_or(x) ,unwrap_or_default() или просто .or()), а при помощи сторонних либ (anyhow , thiserror и пр.) можно ещё и контекст ошибки явно указать и делать удобную конвертацию одних ошибок в другие, в том числе и лениво.

      В Zig тоже есть варианты вроде того же элвиса или orfail как в статье или ordefault (по аналогии с растом . При всей похожести кода zig и go наличие лаконичной обработки ошибок в zig делает его заметно приятнее.


      1. NeoCode
        23.07.2024 11:48

        Опционалы в Go фактически существуют.

        В явном виде на уровне языка все-же нет.


    1. vadimr
      23.07.2024 11:48

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

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


  1. Kenny201
    23.07.2024 11:48

    В go 2 версии хотят для ошибок добавить конструкции check, handle.


    1. mrobespierre
      23.07.2024 11:48

      Go 2 не будет же, сто раз уже подтвердили.


  1. koplenov
    23.07.2024 11:48

    А может просто вызывать метод или, если ошибка, исполнить другой код?

    Например, как это сделано в V :D

    https://docs.vlang.io/type-declarations.html#optionresult-types-and-error-handling

    Пример из статьи выглядел бы так:

    // вызываем функцию, а если ошибка - паникуем
    data := read_from_db() or { panic(err) }
    
    // ещё вызываем функцию, но необрабатываем ошибку если она есть - !
    processed_data := process(data)!
    
    // вызываем функцию, а если ошибка, то обрабатываем её в этом блоке - например, вызываем функцию do_something с передачей ошибки в качестве аргумента
    write_result(processed_data) or {
      do_something(err)
    }
    

    и никаких вам if (err != nil) :>


    1. koplenov
      23.07.2024 11:48

      Конструкция or

      Может обработать ошибку и вернуть значение по умолчанию:

      fn do_something(s string) !string {
          if s == 'foo' {
              return 'foo'
          }
          return error('invalid string')
      }
      ​
      a := do_something('foo') or { 'default' } // a будет 'foo'
      b := do_something('bar') or { 'default' } // b будет 'default'
      println(a)
      println(b)
      

      Может раньше прервать выполнение:

      user := repo.find_user_by_id(7) or { return }
      

      Может исполнить какой-то другой код:

      user := repo.find_user_by_id(7) or {
        log("not found", err)
        return
      }
      

      и просто, и гибко :D

      а это ещё не зашла речь про panic, recover и defer..


    1. olivera507224
      23.07.2024 11:48
      +2

      Подозреваю, что этому может препятствовать целый ряд факторов:

      1. Функция не обязана возвращать ошибку.

      2. Функция не обязана возвращать результат и вторым значением ошибку. Ошибка может быть на любом месте возвращаемых значений.

      3. Функция не обязана возвращать только одну ошибку. Их может быть и больше.

      4. Если функция возвращает ошибку, это вовсе не говорит о том, что эта ошибка "произошла" при выполнении функции. Возможно, данная функция - это всего лишь фабрика ошибок. Об этом может знать только сам разработчик.


      1. koplenov
        23.07.2024 11:48

        1. Функция не обязана возвращать ошибку.

        2. Функция не обязана возвращать результат и вторым значением ошибку. Ошибка может быть на любом месте возвращаемых значений.

        Хорошо, можно же просто пометить функцию, как не возвращающую ошибку, верно?

        fn strong_function() string {
          return "some"
        }
        
        1. Функция не обязана возвращать только одну ошибку. Их может быть и больше.

        Go way нам сказал, что это ошибка - это просто просто значение
        А значит, мы можем просто вернуть с одной функции несколько значений, верно?

        fn more_errors_function() (string, int) {
          return "i am eror", 42
        }
        err1, err2 := more_error_func()
        println(err1)
        println(err2)
        
        1. Если функция возвращает ошибку, это вовсе не говорит о том, что эта ошибка "произошла" при выполнении функции. Возможно, данная функция - это всего лишь фабрика ошибок. Об этом может знать только сам разработчик.

        А значит код в блоке or {} - мы определяем сами, верно? :D


    1. makarychev_13
      23.07.2024 11:48

      user, err := repo.GetUserByID(id)
      if errors.Is(err, pgx.ErrNoRows) {
        return //
      }
      if err != nil {
        return //
      }

      Такой подход плохо работает для такого случая


  1. lcat
    23.07.2024 11:48

    Error wrap и error.as, error.is не решают разве часть озвученных проблем?


    1. Sanchous98
      23.07.2024 11:48

      Только часть. Проблему бойлерплейта if err != nil не решила


  1. bromzh
    23.07.2024 11:48
    +3

    А ведь решение простое - купить педали для игр, и забиндить одну из них на вставку сниппета if err != nil. Go-way?


    1. domix32
      23.07.2024 11:48
      +1

      Так проблема в первую очередь не писать, а читать. А во вторую - конвертация одних ошибок в другие.


      1. bromzh
        23.07.2024 11:48
        +1

        На вторую педаль назначаем фолдинг этих строк. Или окрашиваем их в некотрнастные цвета, чтобы были на втором плане.

        Ну не язык же менять, в самом деле.


  1. Megadeth77
    23.07.2024 11:48

    А можно просто для информации - почему туда нормальные исключения не завезут? Которые по сути и есть избавление от этого бойлерплейта?


    1. event1
      23.07.2024 11:48
      +2

      Нормальные — это как в питоне или как в джаве? Если как в питоне, то у функций появляется большая, важная, но неявная часть интерфейса. Если как в джаве, то везде надо декларировать миллионы потенциальных исключений. В силу этих соображений разработчики Go приняли смелое решение избавиться от исключений вовсе. Судя по всему, получилось тоже плохо.


      1. xxxDef
        23.07.2024 11:48
        +1

        В c# не надо ничего декларировать.


      1. Megadeth77
        23.07.2024 11:48
        +1

        Понятно, спасибо. Кажется если уж упарываться по явности, то даже checked/unchecked exceptions лучше этой пляски с err!=nil , по крайней мере happy path не забивается обработкой ошибок. А так непонятно, зачем эта явность нужна. В питоне, несмотря на дзен, таки забили на декларацию исключений, и правильно сделали. Потому что дзен это прекрасно, но с инструментом должно быть удобно работать в первую очередь.


  1. asatost
    23.07.2024 11:48
    +1

    Если честно, не очень понятно, почему Вы хотите, чтобы Вам завезли реализацию именно на уровне языка. Особенно первой проблемы.

    Если конструкция if err != nil { ... } постоянна, то разве реализация на уровне редактора кода не проще?

    Будет у Вас что-нибудь типа: прочитали из базы() #err тычком на #err развернётся обработчик ошибок. Ну и пару кнопок/хоткеев: раскрыть/свернуть все обработчики ошибок, вставить блок обработки ошибок.

    И итоговый код типа такого:

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


  1. JekaMas
    23.07.2024 11:48

    У меня проблема ушла с появлением ассистентов, copilot отлично предлагает валидный блок `if err != nil` с подходящими текстом и данными.


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

      copilot не помогает в чтении кода


  1. 1ee
    23.07.2024 11:48

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

    А если еще и функции закрыты интерфейсом и у него может быть несколько реализаций, тогда первый вариант кажется очень не надежным. Первое желание - засунуть его в горутину, поставить обработчик паники и сделать issue чтоб автор переоформил код.


  1. xxxDef
    23.07.2024 11:48
    +2

    С ужосом вспоминаю годы програмитрования на c++ для COM. Все эти if (FAILED(hr)) return hr; после каждого вызова функции.

    Помнится посмотрел в сторону go с его декларируемой "простотой", увидел то же самое, ужаснулся и больше никогда.

    Множество языков было загублено желанием авторов следовать каким то академическим догмам, превращая в итоге программистов в операторов бойлерплейта. С++, java - это то что приходит на ум сразу, а так тыщщи их.