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

Проблема: Избыточная проверка ошибок

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

result, err := someFunction()
if err != nil {
    return err
}

Такой подход выглядит аккуратно в изоляции, но при масштабировании кодовой базы он приводит к избыточности. Множественные проверки if err != nil разбросаны по всему коду, что делает его загроможденным и сложным для поддержки.

Почему Go использует такой подход

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

Пример избыточности

Рассмотрим сценарий, где выполняется несколько последовательных вызовов API:

user, err := getUser(id)
if err != nil {
    return err
}

posts, err := getUserPosts(user.ID)
if err != nil {
    return err
}

comments, err := getComments(posts[0].ID)
if err != nil {
    return err
}

Каждый вызов функции сопровождается проверкой if err != nil, что делает код многословным и сложным для восприятия.

Решение: Использование функции Must

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

Шаг 1: Создание функции Must

Функция Must принимает значение любого типа и ошибку. Если ошибка не равна nil, функция вызывает панику.

// utils.go
package utils

func Must[T any](value T, err error) T {
    if err != nil {
        panic(err)
    }
    return value
}

Шаг 2: Использование функции Must

Теперь можно использовать Must для упрощения вызовов функций:

package main

import (
    "fmt"
    "must_example/utils"
)

func main() {
    user := utils.Must(getUser(1))
    posts := utils.Must(getUserPosts(1))
    comments := utils.Must(getComments(1))

    fmt.Println("User:", user)
    fmt.Println("Posts:", posts)
    fmt.Println("Comments:", comments)
}

Преимущества подхода

  • Уменьшение избыточности: Проверка ошибок централизована в одной функции.

  • Улучшение читаемости: Основная логика не загромождена проверками ошибок.

  • Быстрая отладка: Паника сразу указывает на проблему.

Осторожность с Must

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

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


  1. ednersky
    09.02.2025 17:11

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

    Как по мне, так:

    • Делегирование обработки ошибки вышестоящему коду - крайне частый паттерн (настолько частый, что в Rust даже оператор ? придумали чтоб укоротить запись if err return)

    • Делегирование обработки ошибки вышестоящему коду НИЧЕМ не отличается (по смыслу) от исключений.

    • Исключения могут быть выражены через обработку ошибок, а обработка ошибок через исключения. То есть тезисы о скорости, перфомансе и прочем неактуальны

    • При этом, исключения дают куда более лаконичный код.

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

    Итого,

    Переход к обработке ошибок - ошибка, которая заставляет программиста делать работу за компилятор. Которая снижает читабельность и увеличивает сложность кода.

    Автору респект за статью. В современное время подобное публиковать - нужно некоторое количество смелости: хейтеры (или адепты) могут и закопать.

    PS: есть модуль: https://pkg.go.dev/github.com/n-mou/yagul@v0.1.6/g


    1. che1nov Автор
      09.02.2025 17:11

      Согласен, публиковать статьи, которые идут вразрез с популярными мнениями, требует смелости.)

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


    1. slava0135
      09.02.2025 17:11

      >Исключения могут быть выражены через обработку ошибок, а обработка ошибок через исключения. То есть тезисы о скорости, перфомансе и прочем неактуальны

      Каким они могут быть неактуальны, если исключения всегда работают хуже (хотя бы потому что сохраняют стек вызовов (забудем про C++)). А вообще - исключения частный случай алгебраических эффектов (см. язык Koka).

      >При этом, исключения дают куда более лаконичный код.

      До тех пор пока не нужно их обрабатывать (try catch finally не вершина лаконичности). И все это ценой отсутствия типа ошибок в сигнатуре функций (забудем про Java и checked exceptions, которые почему-то все ругают) - пишите в каждой функции try или все может упасть в неподходящий момент.

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

      Исключения и стек вызовов - вещи ортогональные (тут пора вспомнить про C++, где стека нет).

      В расте можно включить стек вызовов через RUST_BACKTRACE=1.

      fn main() {
          foo();
      }
      
      fn foo() {
          bar();
      }
      
      fn bar() {
          panic!("oops");
      }
      Stacktrace (запускал в Playground)
      Stacktrace (запускал в Playground)

      Вообще стеки вызовов удобны для программиста, но не удобны для пользователя - можно создавать собственные стеки ошибок (добавлять контекст), например через anyhow https://crates.io/crates/anyhow. В golang практикуются похожие вещи, и все счастливы.


      1. ednersky
        09.02.2025 17:11

        Каким они могут быть неактуальны, если исключения всегда работают хуже (хотя бы потому что сохраняют стек вызовов (забудем про C++)). А вообще - исключения частный случай алгебраических эффектов (см. язык Koka).

        1. стек вызовов ВСЕГДА сохранять нет необходимости, его можно восстановить только по требованию

        2. все эти алгебраические эффекты - это попытка справиться с кандалами, которые сами на себя надели: смешивать ошибки с возвращаемыми значениями банально неудобно, противоестественно.

        Человеку гораздо естественнее описать happy-way - "как должно быть", а затем тупо попадать везде, где не happy (этот код будет как бы за пределами основного), ну и разобрать 1-2 ошибки которые действительно нужно разобрать.

        но адепты функциональных языков программирования сперва сказали:

        • мутабельность - плохо!

        Затем мучаясь с этим тезисом, придумали себе рекурсию, хотя немутабельная программа В РЕАЛЬНОМ МИРЕ никогда не выигрывает у мутабельной: читабельность хуже, работа с параллельными потоками тоже хуже, хотя мутабельность позиционируется именно про них.

        Затем будто им мало, продекларировали:

        • Типы - это серебряная пуля!

        И потащили к себе ненужные типы и айда смешивать в алгебраический тип данных все в том числе несовместимые сущности: данные и ошибки!

        выглядит как секта, действует как секта, что это? Секта и есть! Секта свидетелей функционального программирования.


        1. slava0135
          09.02.2025 17:11

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

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

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


          1. ednersky
            09.02.2025 17:11

            Не знаю зачем вы начали говорить про мутабельность

            иммутабельность — главный признак Функционального Программирования

            правда свидетели его в последнее время ещё и о АДТ с монадами упоминают


        1. DasMeister
          09.02.2025 17:11

          Тем временем в реальном мире немутабельные программы давно выиграли у мутабельных.

          Т.к. практически все современные СУБД строятся на иммутабельных структурах. Даже там где есть мутабельность (postgresql) - это изменение пару бит, реально данные не меняются и там.

          Большая часть современных программ - вообще stateless (особенно тех, что пишутся на го).

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


          1. ednersky
            09.02.2025 17:11

            все современные БД — квинтэссенция мутабельности

            пишут их на Си, иногда включая другие языки, как клей — луа там, питон итп

            Неопсредственная мутабельность - нужна

            практически везде, где программа моделирует мир или взаимодействует с миром

            ибо мир мутабелен в своей основе (смотри второй закон термодинамики)


            1. slava0135
              09.02.2025 17:11

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

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


              1. ednersky
                09.02.2025 17:11

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

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

                вот смотрите, что происходит в реальном мире.

                1. Взять полноценную БД с транзакциями и девушками лёгкого поведения у Вас не получается, по причине требований к масштабируемости. Итого, вместо, скажем, постгриса вы берёте набор шардов из чего-нибудь там.

                2. в итоге транзакций у вас нет

                3. но вам нужно "списать товар с полки" или "перевести деньги со счёта 1 на счёт 2"

                4. чтобы это как-то делать, вы берёте ещё один инструмент - очередь

                5. и задача из "рассчитать что нужно в транзакции и закоммитить её" вырождается в несколько задач:

                  1. положить таску в очередь

                  2. повторять её выполнение до успеха между распределёнными шардами

                  3. сохранять при этом идемпотентность (которая зачастую не чистая, а тоже мутабельная)

                  4. результат - тоже таска в обратной очереди

                  5. клиенту подождать обратной таски

                  6. вернуть ответ пользователю

                и вот в этой хрени чистые функции конечно можно повыделять, но их там будет дай боже 10%.

                а основное - тесты, причём интеграционные: в контейнерах подымаем всех соседей, очереди, шарды, аналитику и моделируем реальные ситуации "на плоке пирожка не было".

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


      1. ednersky
        09.02.2025 17:11

        В расте можно включить стек вызовов через RUST_BACKTRACE=1.

        ага и в примере Вы написали что? panic! то есть исключения.

        ЧИТД.

        я же так и написал: у исключений есть возможность включить показ стека вызовов, а у ошибок возвращённых вручную - только если вручную же собирать errorchain, то есть вместо

        if err != nil {
          return err
        }

        Писать:

        if err != nil {
          return errors.Chain("ошибка вызова bar", err)
        }

        плюс во все конструкторы Error всунуть сохранение стека. Но это уже ГАРАНТИРОВАНО будет хуже исключений: и по перфомансу и по нагрузке на программиста. Впрочем по нагрузке на программиста оно уже хуже


      1. DasMeister
        09.02.2025 17:11

        В го нынче на уровень линтеров (кодстайла) вытащили требование не только разворачивать стек при ошибки в ручную (return err), но ещё и содержать почти полный стек (fmt.Errorf("kokoko: %w", err).

        Насчёт все счастливы, интересное утверждение. Учитывая что proposal's с аналогичным растовому синтаксису "?" были весьма популярны.


        1. ednersky
          09.02.2025 17:11

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

          foo-bar-baz

          some-other-baz

          и так далее

          и потому стек нужен не линтеру, а рантайм упавшему коду.


      1. funny_falcon
        09.02.2025 17:11

        Вы упомянули Koka. Вы его уже для чего-нибудь применяли? Есть впечатления от практического использования?


        1. slava0135
          09.02.2025 17:11

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


  1. XelaVopelk
    09.02.2025 17:11

    Этак в скорости и до монад доберётесь: https://habr.com/ru/articles/339606/


    1. ednersky
      09.02.2025 17:11

      с монадами в разы сложнее, чем с ошибками и исключениями

      монады неестественны, а потому безобразны

      (надеюсь, аллюзия понятна)


      1. XelaVopelk
        09.02.2025 17:11

        я к тому, что половину пути до Either монады автор уже прошёл. :) Ну а насчёт "безобразности" - на вскус и цвет фломастеры разные.


        1. ednersky
          09.02.2025 17:11

          монады - это безысходность.

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

          но вот на вопрос "нахрена?" ответа, увы нет.


  1. NightShad0w
    09.02.2025 17:11

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

    func ParseIP(s string) IP , но func ParseMAC(s string) (hw HardwareAddr, err error) Ну как так-то.


    1. gudvinr
      09.02.2025 17:11

      Ну как так-то

      Потому что обратная совместимость


  1. 411
    09.02.2025 17:11

    Идея Must правильная, реализация - нет. panic не для этого.

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


    1. ednersky
      09.02.2025 17:11

      делегирование ошибки вышестоящему обработчику абсолютно изоморфно исключеням

      но при этом:

      • куда менее лаконично (более многословно)

      • при решении через АДТ получаем привет от возможных ошибок типов


  1. Noah1
    09.02.2025 17:11

    Замена ошибок на панику? Поздравляю, вы испортили обработку ошибок. Паники используются в исключительных случаях, ошибки в 90% нужно возвращать наверх.

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

    Многословно - да, не читаемо - ни в коем случае.

    Поражает упорство с которым люди пытаются придумать велосипед поверх предельной простой системы обработки ошибок, ну нет тут '?' из раста, ну смиритесь, либо поменяйте язык, если if err != nil вас так корёжит.


    1. ednersky
      09.02.2025 17:11

      Паники используются в исключительных случаях, ошибки в 90% нужно возвращать наверх.

      1. кому нужно?

      2. а этот «верх» в 90% случаев тоже возвращает их наверх, тем самым приводя ошибки к исключениям

      и получается, Вы тоже пришли к поздравлению:

      Поздравляю, вы испортили обработку ошибок.


      1. Noah1
        09.02.2025 17:11

        1. Всем нужно, никто не будет доволен если библиотека внутри себя вызовет нежданную панику.

        2. Да, чаще всего именно это и нужно, похоже это на исключения или нет - роли не играет.

        и получается, Вы тоже пришли к поздравлению:

        Ни коим образом, я предлагаю использовать ошибки так, как их задумывали авторы языка. Автор статьи лепит велосипед, пытаясь превратить язык в, знакомый ему, javascript/python.


        1. ednersky
          09.02.2025 17:11

          Всем нужно, никто не будет доволен если библиотека внутри себя вызовет нежданную панику.

          а если она выдаст задокументированную панику, то всё ok.

          Да, чаще всего именно это и нужно, похоже это на исключения или нет - роли не играет.

          помните? "плавает как утка, крякает как утка..."

          соответственно, получается, роль играет

          Ни коим образом, я предлагаю использовать ошибки так, как их задумывали авторы языка.

          мы обсуждаем вопрос, что авторы языка могут ошибаться и ошиблись. пройдёт несколько лет и Rust с Go будут вытеснены языками, в которых громоздкие АДТ будут заменены лаконичными исключениями. В которых неудобный err и defer, заменится на удобный try/catch/finally/throw


          1. Noah1
            09.02.2025 17:11

            а если она выдаст задокументированную панику, то всё ok.

            Да, это ок, если произошло то, из-за чего библиотека не может функционировать, и это задокументировано - паника ожидаема.

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

            Авторы языка не ошиблись с дизайном, он простой и лаконичный. Было несколько недочётов, обработка ошибок - не один из них.

            В которых неудобный err и defer, заменится на удобный try/catch/finally/throw

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


            1. ednersky
              09.02.2025 17:11

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

              эта мантра от повторения не становится реальностью

              у нас есть простой лакончиный

              func foo() any {
                a := bar()
                b := baz()
                c := some()
                return a + b + c
              }

              И есть втрое более сложный вариант с if err != nil после каждого вызова функции.

              Соответственно, правильно говорить так: Авторы ХОТЕЛИ чтобы язык имел простой и лаконичный дизайн. Однако авторы попали в ловушку, что автор компиляторов != его пользователь. Автор баз данных != её пользователь. И у них не получилось банально потому, что авторы крайне слабо представляли себе предметную область эксплуатации языка. Им казалось, что все вокруг пишут именно компиляторы (или именно базы данных). Но большинство пишет микросервисы.


              1. Noah1
                09.02.2025 17:11

                От адептов исключений всегда одна и та же ошибка: код, который ты привел в пример, не делает тоже самое, что и if err!=nil. Попробуй отлавливать исключения(паники) индивидуально на каждую функцию, я посмотрю что получится.

                И есть втрое более сложный вариант с if err != nil после каждого вызова функции.

                Если 3 if - это сложно, то мои глубочайшие соболезнования.

                Авторы ХОТЕЛИ чтобы язык имел простой и лаконичный дизайн. Однако авторы попали в ловушку

                Никто никуда не попал, все довольны и с радостью пользуются одной из самых понятных систем работы с ошибками. И только новички, привыкшие к ужасам js/Java/python поначалу трясутся от "ошибок как значений" и отсутствия сахара.


                1. ednersky
                  09.02.2025 17:11

                  Попробуй отлавливать исключения(паники) индивидуально на каждую функцию, я посмотрю что получится.

                  будет нечто вроде

                  a, err := abc.PCall(bar)
                  if err != nil {
                     return nil, err
                  }
                  
                  b, err := abc.PCall(baz)
                  if err != nil {
                     return nil, err
                  }
                  
                  c, err := abc.PCall(some)
                  if err != nil {
                     return nil, err
                  }
                  
                  return a + b + c, nil

                  Если 3 if - это сложно, то мои глубочайшие соболезнования.

                  • 3 if выводят этот код за пределы экрана

                  • 3 if умножают сложность кода втрое

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

                  если мантру повторять, то можно опровергнуть и действительность


                  1. funny_falcon
                    09.02.2025 17:11

                    • да, выводят за пределы экрана.

                    • нет, усложнение не «втрое»

                    Я поймал себя на том, что, читая код, пропустил все if err != nil.

                    Да, читать менее удобно, и в один glimpse код не помещается. Но разница не в три, и даже не в два раза. Максимум полтора, а то и процентов 10-15%.


                    1. ednersky
                      09.02.2025 17:11

                      Но разница не в три, и даже не в два раза.

                      • было три строки присвоения

                      • затем после каждой появилось по три строки проверки err и возврата (го форматтер запрещает писать эти строки "в одну" - только в три)

                      • то есть три строки превратились в 12

                      итого, err увеличивает не в три, и даже не в два раза, а во все четыре!


            1. ednersky
              09.02.2025 17:11

              И вообще с каких пор defer плох?

              defer хорош - это форма finally. плох же он тем, что нужно было делать defer { block } а сделали неудобный defer foo()


              1. Noah1
                09.02.2025 17:11

                Какая разница? Это одно и тоже. Вместо анонимного блока - объявляешь анонимную функцию с понятной областью видимости.


                1. ednersky
                  09.02.2025 17:11

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


                  1. funny_falcon
                    09.02.2025 17:11

                    А так блок в блок бы вкладывал. Разница чисто синтаксическая, механизм был бы тем же, скорее всего.

                    Мне не нравятся Го-шные defer не из-за синтаксиса, а из-за привязки к времени жизни функции. Привязка к блоку, как в большинстве языков (RAII, или finally, или defer, где он тоже есть) выглядит намного органичнее.

                    Я понимаю, почему было привязано к функции: 1. так проще реализовать (просто список колбэков без сложностей со стороны компилятора), 2. изредка удобнее (если инициализация ресурса и, соответственно, defer на его «отпускание» зависят от условия).

                    Но всё же привязку к блоку хочется чаще.


                    1. ednersky
                      09.02.2025 17:11

                      выбрав функцию, они решили вопрос локализации её аргументов

                      for i := 0; i < 10; i++ {
                           defer fmt.Printf("defer %d", i)
                      }

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

                      в общем разработчиков Go я понимаю:

                      • им лень было возиться с динамической типизацией (хотя потом частично any/interface жизнь их заставила сделать) и они привязали всё к типам

                      • им лень было возиться с локализацией контекста defer и они сделали функциональный синтаксис

                      • им лень было сделать нормальные исключения и они извратились как у них получилос

                      • им лень-лень-лень-лень было, и получилось это УГ


                      1. evgeniy_kudinov
                        09.02.2025 17:11

                        Так как Go — это open-source, то возможно:

                        1. Сделать форк и назвать его MyIdealGo.

                        2. Запилить все фичи, которые, кажется, облегчат жизнь.

                        3. Распространить примеры (что, как, зачем).

                        4. Создать PR и убедить мантейнеров языка в нужности фич.

                        5. Profit.

                        Пожелаю Вам заранее успеха)


                      1. ednersky
                        09.02.2025 17:11

                        зачем? я наблюдал смерть многих языков, осталось чуть подождать и Go/Rust тоже умрут.

                        на их место придёт язык с исключениями :)

                        и все будут ходить (так же строем) и рассказывать по форумам, что исключения - это здорово, а вот ошибки были неудобны.


          1. qrKot
            09.02.2025 17:11

            если она выдаст задокументированную панику, то всё ok.

            И вместо if err != nil на каждый вызов будем писать if r := recover(); r != nil

            Ну спасибо, ну удружил! (на самом деле нет)


            1. ednersky
              09.02.2025 17:11

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

              если это микросервис, то это будет middleware, которое будет оформлять логи и 500 ошибку.


  1. evgeniy_kudinov
    09.02.2025 17:11

    Странное понимание о читаемости кода.

    comments, err := getComments(posts[0].ID)
    if err != nil {
        return err
    }
    
    comments := utils.Must(getComments(1))

    В первом варианте видно явные намерения, как во втором неявное.
    И думаю не надо использовать panic вместо throw, его поведение отличается.
    Надеюсь, кто такое предлагает, не использует на реальных проектах, в которых участвуют много людей.
    Если не нравится реализация данной концепции в go, возьмите подходящий инструмент. Как вариант https://harelang.org/tutorials/introduction#handling-errors


    1. ednersky
      09.02.2025 17:11

      В первом варианте видно явные намерения, как во втором неявное.

      первый вариант втрое (втрое!) длиннее

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


      1. evgeniy_kudinov
        09.02.2025 17:11

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

        Сугубо личное мнение, что мало букв не равно лучшее понимание.


      1. Noah1
        09.02.2025 17:11

        первый вариант втрое (втрое!) длиннее

        И что? Clear is better than clever.

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

        Нет.


        1. anaxita
          09.02.2025 17:11

          плюс IDE умеет сворачивать этот блок в 1 строку


    1. qrKot
      09.02.2025 17:11

      После utils.Must(..... кусок важный забыли!

      if r := recover(); r != nil { // TODO handle error here } - вот этот)


  1. DasMeister
    09.02.2025 17:11

    Проблема такого подхода в том, что вы не решаете единственную причину, по которой error тип вообще существует в go.

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

    Сигнатура функции чётко говорит о том, что функция может отработа нештатно вернув error тип.

    Сама по себе panic'a может быть вообще чем угодно - интерфейсом error, строкой, числом, набором байт - чем угодно. Это слишком низкоуровневый примтив, чтобы им можно было бы пользоваться как "исключением".

    Эту проблему можно решить через recover паники в дефёре и превращение его в ошибку для публичной функции/метода. ваша Must функция может вообще именоваться как Try, а дефёр функция как Catch.

    func Something(ctx context.Context, arg1 int, arg2 string) (empty Data, err error)
      defer dry.Catch(&err)
    
      var res Data
      res.Call1 = dry.Try(Func1(ctx, arg1))
      res.Call2 = dry.Try(Func2(ctx, arg2))
      return res, nil
    }

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

    Как оборачивать исключения выброщенные непосредственно вами, как оборачивать паники полученные не от вас, что делать с стек-трейсом (для паники, которая не является error типом, неплохо бы передавать наверх стейт оригинальной паники).

    Просто написать panic - никогда не бывает достаточно.


    1. ednersky
      09.02.2025 17:11

      Как оборачивать исключения

      можно пойти, например, по дороге, по которой пошёл Lua

      то есть реализовать xpcall

      func XPCall(cb func(), cbe func(e error)) {
         // вызвать cb, а потом, если паника - позвать cbe    
      }

      ну и просто pcall тоже можно

      func PCall(cb fun()) error {
           // вызвать cb, а при панике вернуть её в виде error
      }


      1. DasMeister
        09.02.2025 17:11

        Это тот же самый catch'er положенный в defer. Который просто потребует больше кода, от пишущего код.


        1. ednersky
          09.02.2025 17:11

          Ещё раз: в большинстве случаев обрабатывать ошибки на каждом шаге НЕ ТРЕБУЕТСЯ.

          это настолько всем очевидно, что даже упёртые лбы в Rust это поняли и приделали оператор ? в свой язык.

          Правда это им не сильно помогает. Но я отвлёкся.

          поскольку возня с ошибками на каждом шаге не требуется, то достаточно ловить ошибки где-то в одном месте: в функции main, либо в middleware http-сервиса, либо в подобном месте. И всё.


  1. MixaMen
    09.02.2025 17:11

    Давайте начнём с основ. Для чего используют Го? Для написания сервисов. Сервис - это программа, которая постоянно запущена и обсуживает множество запросов. Пришёл запрос, отдаём ответ. Есть ли в работе сервиса место панике?

    Но сервисы действительно бывают большими и сложными. Всегда можно выделить базовые функции: получить пользователя, получить связанные данные. Может случится, что нет такого пользователя. Или нет подключения к базе данных. Только вот в рамках запроса надо ведь не в панику падать, а давать конкретный ответ. И принимать следующий запрос и давать на него ответ. А в фоне можно и переподключится к БД.

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


  1. qiper
    09.02.2025 17:11

    Не традиционный подход

    А какой?