Pull to refresh

Comments 94

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

if err != nil {

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

}

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

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

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

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

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

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

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

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

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

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

прочитали из базы()
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Именно :-)

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

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

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

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

Если посмотреть реакции на все эти proposals, то можно увидеть, что большинство из них задизлайкано сообществом Go.

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

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

https://habr.com/ru/specials/713190/

https://go.dev/blog/survey2024-h1-results

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

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

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

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

Опционалы в 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 делает его заметно приятнее.

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

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

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

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

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

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

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

Например, как это сделано в 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) :>

Конструкция 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. Функция не обязана возвращать ошибку.

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

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

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

  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

UFO just landed and posted this here

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

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

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

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

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

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

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

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

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

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

Ну в питоне, как раз дело в собственной истории: там изначально можно было любой объект бросать. Что приемлемо для языка для быстрого прототипирования. А когда третий делали, видимо, не стали переделывать.

В силу этих соображений разработчики Go приняли смелое решение избавиться от исключений вовсе.

А есть пруф?

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

Разработка Go началась в сентябре 2007 года. В 2007 не было исключений?

Посмотрите доклад Филиппа Кулина

Спасибо, но смотреть почти час как-то лень. Останусь при своём мнении, что Go - просто плохо спроектированный язык. И не потому что там нет исключений

Ну ок. Только Golang это компиляция огромного опыта его создателей, которые разрабатывали языки с 70-х (если не 60-х), в том числе С. И писали на них ОС (Plan-9). Первый компилятор Go был взят из Plan9 кстати.

Ну чтож, эта компиляция опыта почему-то повторила многие ошибки прошлого. И пресловутую ошибку на миллиард из Java. И "нам не нужны дженерики -> ой, нам нужны дженерики" тоже из Java. И npm-hell. И фигово спроектированное АПИ для винды, потому что чуваки как раз из Plan 9, и видимо всё затачивали для юникс.

В конечном итоге, язык-то новый, релизнулся в 2009. Зачем мне, как разработчику, для которого это просто инструмент, знать историю и подоплёки появления языка? Я им пользоваться собрался. И причины, по которым сделано так а не по-другому не так уж важны, важнее результат. А он так себе.

Ну да, под Unix. На backend в основном он.

А после ввода дженериков в итоге ими редко пользуются.

Если что сам сейчас fulltime на golang прогаю.

А историю знать желательно, так как это один из маркеров твоего профессионализма, как сейчас говорят софт скиллы.

Ну ваш коммент только подтверждает, что язык плохо спроектирован. Вроде мультиплатформенный язык, но на винде АПИ кривое? Аргумент типа "Ну у меня в юниксах работает, хз что там жалуются". Не пользуются дженериками? Может потому что они неудобные? И прикручены сбоку, а не изначально запланированы? Хотя в Java почему-то дженерики стали стандартом для использования.

Ну да, под Unix. На backend в основном он.

И даже там чуваки с огромным опытом смогли накосячить.

TL; DR: Пути в unix могут быть любым набором байт. То есть не всегда строками. А в Го вся работа с ФС использует строки.

То есть люди, буквально создававшие ОС не знают, как там устроено и как писать нормальное АПИ для своего же языка? Или как воспринимать их опыт?

Пути в unix могут быть любым набором байт. То есть не всегда строками. А в Го вся работа с ФС использует строки.

полностью согласен с вами в остальном, но людей которые создают пути из произвольных байт надо отлучать от компьютера на 25 лет без права переписки. А те, кто запрещает такое — молодцы.

It's because exceptions thread an invisible second control flow through your programs making them less readable and harder to reason about.

Никаких "тех времён" или чего-то подобного. Сознательное решение убрать исключения потому что это неявный поток исполнения. Потому что оба решения с исключениями плохие. В гугле это [было] видно особенно явно, учитывая объёмы кода на питоне.

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

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

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

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

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

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

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

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

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

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

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

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

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

если не нравятся if err != nil, а вместо этого хочется исключения (или что-то похожее на исключения), то код бизнес-логики можно преобразовать в continuation-passing style

// логика в виде матрёшки
func example_CPS() {
	userId := 100500
	onError := func(err error) {
		log.Fatal(err)
	}

	readFromDb_CPS(
		userId, 
		func(u *User) {
			processUser_CPS(
				u, 
				func(u *User) {
					saveToDb_CPS(u, func(*User) { log.Println("saved") }, onError)
				}, 
				onError,
			)
		}, 
		onError,
	)
}

// или немножко в арабском стиле: продолжения определяем задом наперед
func example_CPS_inversed() {
	userId := 100500
	onError := func(err error) {
		log.Fatal(err)
	}

	processUserCont := func(user *User) {
		saveToDb_CPS(user, func(*User) { log.Println("saved") }, onError)
	}

	readFromDbCont := func(user *User) {
		processUser_CPS(user, processUserCont, onError)
	}

	readFromDb_CPS(userId, readFromDbCont, onError)
}
Hidden text
func readFromDb_CPS(id int, onSuccess func(*User), onError func(error)) {
	if some_db_error {
		onError(errors.New("not found"))
	} else {
		onSuccess(&User{})
	}
}

func processUser_CPS(user *User, onSuccess func(*User), onError func(error)) {
	if user == nil {
		onError(errors.New("user is nil"))
	} else {
		onSuccess(user)
	}
}

func saveToDb_CPS(user *User, onSuccess func(u *User), onError func(error)) {
	if user == nil {
		onError(errors.New("user is nil"))
	} else {
		onSuccess(user)
	}
}

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

Мне кажется в эпоху loki/kibane/etc и все большего развития трейсинга, достаточно:

  1. в месте ошибки создать трейс (как и выше по коду)

  2. опционально: написать в лог ошибку с данными достаточными для воспроизведения проблемы (+файл:строка+trace_id)

  3. прокинуть нормальную кастомную ошибку выше, такую как LoadDBError{ error: err, sql: sql, etc... }

loki покажет полный набор логов по конкретному trace_id и для этого не нужно делать враппинг самому + трейсы покажут откуда пришел запрос и прочие данные

Я считаю, что обрабатывать и логгировать ошибку лучше прямо в месте её возникновения. Всё остальное, что напридумывали с try-catch и optional типами - это, по-моему, от лукавого, потому что это отдаляет вас от места ошибки, даёт вам возможность временно забить на обработку ошибок, переложить ответственность на что-то, что когда-то там обработает ошибку и что-то там сделает с этим. Тогда уж ваще лучше паники кидать по этой логике или превратить Go в Java.

Раскручивая стек ошибок с самого верха, вы теряете самое ценное, что у вас есть - время...

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

Sign up to leave a comment.