Comments 23
Коллега, Ваше стремление упростить код похвально, но я не думаю, что ваша библиотека - это Go way.
Язык, пусть и новый, но всё же имеет стандарты и устоявшиеся паттерны. Не уверен, что опытные разработчики, читая подобный код будут легче его понимать, так как уже привыкли к классике if err!=nil.
Мне, например, читать код с новыми конструкциями try было сложнее, чем классический код с проверками ошибок на nil.
Но библиотека интересная, обязательно попробую её потестить)
Понимаю ваши сомнения, коллега. Действительно стандартные конструкции if err != nil стали неотъемлемой частью Го и привычны большинству разработчиков.
Но try я создал не для того, чтобы заменить устоявшиеся практики, а чтобы предложить альтернативу для случаев, когда хочется сократить шаблонный код, сделать логику более линейной, а код более декларативным, что ли.
Конечно, всё новое требует привыкания. Я, например, уже настолько привык, что теперь жить не могу без нее -) Все эти if err!=nil кажутся перегруженными.
В любом случае буду рад, если попробуете либу на практике и поделитесь своим опытом.
С
try
ваш код станет проще и элегантнее, а жизнь разработчика — чуточку легче!
Точно?
Чет пример: return try.Val(fn2(try.Val(fn1(...)))) не кажется простым и элегантным...
Да, действительно пример может выглядеть более компактным, но не обязательно более "элегантным" на первый взгляд. Если попробовать подставить реальные функции, то может не так и страшно получится.
Например:
return try.Val(strconv.Atoi(string(try.Val(os.ReadFile(name)))))
А если еще и импорт использовать с точкой, то получается еще компактнее:
return Val(strconv.Atoi(string(Val(os.ReadFile(name)))))
Это вопрос дискуссионный, но выскажу свое имхо, мне лично все же кажется, что код выше выглядит более "элегантно" чем портянка:
data, err := os.ReadFile(name)
if err != nil {
return 0, err
}
n, err := strconv.Atoi(string(data))
if err != nil {
return 0, err
}
return n, nil
Идея try — дать возможность убрать часть рутинных проверок и сделать код линейнее, но соглашусь, что иногда читаемость страдает, особенно когда вложенные вызовы начинают напоминать матрёшку.
Этот подход работает не всегда и не для всех, и try — скорее инструмент, который можно использовать, когда подходит к контексту задачи. Если он упрощает жизнь — отлично, если нет — Go-way с if err != nil по-прежнему остаётся классикой.
Может, в будущем найду способ сделать пример с вложенными вызовами более аккуратным.
Спасибо, что обратили внимание!
Кстати, а как на низком уровне работает паника в Go? Аналогично классической раскрутке стека при исключении в С++?
На сколько мне известно, да, паника в Go имеет некоторые сходства с раскруткой стека в C++. Когда возникает паника, Go начинает раскрутку стека вызовов, аналогично исключениям в C++. Она идёт вверх по стеку вызовов, выполняя defer на каждом уровне, пока не найдёт recover, который может её перехватить и остановить раскрутку. Если recover не вызывается, программа завершится.
Т.е. получается, что сам механизм исключений в языке есть, но авторы языка из "дизайнерских" соображений решили не применять его, кроме как в крайних случаях (а такие случаи неизбежны - то же деление на ноль). И совсем без обработки таких "настоящих" исключений тоже как-то нельзя - это резко отбросит язык назад в развитии на несколько десятков лет.
Можно сделать вывод, что раскрутка стека при исключениях совершенно неизбежно должна быть в ядре любого современного языка, даже если в нем нет явной поддержки исключений в стиле С++, а вместо нее реализованы различные "легкие" варианты (явные коды возврата как в Go, или типы с логикой Optional/Result и вспомогательные операторы, как в Rust/Swift/Zig)
Если мне не изменяет память, то где-то прямо жирным шрифтом написано не использовать паники как аналог exception из других языков.
Да да, вы правы, главное правило в Го – Не паникуй!
В Go паника действительно не предназначена для использования в качестве аналога исключений, как в других языках. Основная идея паники в Go — сигнализировать о действительно непредвиденных ситуациях, которые делают продолжение работы невозможным.
Библиотека использует панику как способ избавить от шаблонного кода, сделать его более декларативным. Но я подчеркиваю, что она подходит для редких и непредсказуемых ошибок, а не для всего подряд. В тех местах где ошибки не ожидаются, там где ошибка - это действительно паника. Это компромисс между читабельностью и идиоматичностью Go, и я понимаю, что он не для всех.
Если вы видите try как нарушение философии Go, это вполне оправданная точка зрения. Я создал библиотеку как эксперимент и альтернативу, когда хочется более лаконичного подхода.
Строить библиотеку вокруг паник - очень спорное решение
Однострочники - это прям perl/bash-way: очень приятно писать, классная гимнастика для ума. Но спустя пол года после написания читать этот код в два часа ночи, когда что-то, внезапно, работает не как запланированно - это пытка. Чем проще и "тупее" код, тем его проще читать и поддерживать, а это именно то, на что уходит большая часть времени.
Я бы на картинке поменял огоньки на время а воду из брандспойтов на огонь
Кажется что просматривая go code в разных статьях, уже можно определить какой в голове у человека "сидит" язык программирования и его хочет автор написать на go ) У шарпистов - гошарп, плюсовик- гоплюсы и так далее. (шутка)
А по теме статьи - подход интересный, но думаю не приживется, так как вносит дополнительную абстракцию.
Согласен, часто можно видеть, как разработчики пытаются перенести философию из других языков в Go. Но это точно не про меня. Мне действительно нравится низкоуровневый подход в Го, похожий на C — с обработкой возвращаемых ошибок прямо в коде. Я не большой поклонник try-catch в других языках.
Однако в данном случае это не попытка добавить try-catch в Go. Название пакета могло бы быть и другим, например, must. (в Го ведь уже есть функция MustCompile, которая выбрасывает панику, и никого это не смущает). Название try выбрано скорее для ассоциации с try из других языков, но сам подход всё же остаётся ближе к духу Go — работать с ошибками максимально явно и линейно, а не скрывать их за исключениями.
Спасибо за ваш комментарий, рад, что подход показался интересным -)
Например где точно может быть полезна библиотека - в таких ситуациях, как обработка транзакций или выполнение высокоуровневой бизнес-логики. Когда мы имеем дело с критическими функциями, в которых ошибки не должны происходить в обычных обстоятельствах, каждая ошибка — это повод для полной остановки процесса и отката всех изменений. В таких местах паника может быть вполне оправдана, поскольку ошибка сигнализирует о том, что вся операция должна быть аннулирована, транзакция откатана.
Очень грубый условный пример:
func (c *Context) processOrder() (err error) {
defer try.Catch(&err)
userID := try.Val(parseID64(c.Request.Form.Get("user-id")))
user := try.Val(c.userByID(userID))
try.Require(user != nil, "not found user")
orderID := try.Val(parseID64(c.Request.Form.Get("order-id")))
order := try.Val(c.orderByID(orderID))
try.Require(order != nil, "not found order")
try.Require(user.Balance >= order.Amount, "not enough funds")
user.Balance -= order.Amount
order.Status = OrderStatusDone
try.OK(c.updateUser(user))
try.OK(c.updateOrder(order))
}
Тут видно, как можно декларативно описать шаги выполнения транзакции: провести проверку пользователя и заказа, обновить данные и прочее, и, если что-то идёт не так, сразу прекратить выполнение и откатить транзакцию. При этом в коде даже нет явного понятия error — всё сводится к декларативным инструкциям, а catch автоматически обрабатывает сбой.
Этот подход помогает убрать всю рутинную проверку ошибок и сфокусироваться на том, что функция должна делать в идеальных условиях. А если что-то идёт не так, try позаботится о том, чтобы правильно передать ошибку и отменить изменения.
1) Рядом с проверкой ошибки обычно нужен ещё вывод ошибки в лог (в консоль).
С вашими Try не будет логов, или будут совсем не в том месте где надо.
2) Ошибку надо не просто возвращать, а "оборачивать" - добавлять свои комментарии в текст ошибки
- это тоже не получится.
В общем бесполезно всё это (try), и вредно.
Вот нахрена вы тащите исключения в Го? Креативность похвальна, но уже сто раз было сказано - исключения в Го не нужны ни в каком виде, и не будет их. Я считаю if err != nil одной из самых лучших систем обработки ошибок, предельно просто и максимально эффективно, исключения я забыл как страшный сон.
Не надо тащить привычки из других языков в Го, он крайне opinionated и делает некоторые вещи иначе, ты либо принимаешь "go-way", либо не используешь Го.
Что происходит если в теле функции произошла паника, инициированная не через методы try?
defer try.Catch(&err)
отловит любую панику, преобразует ее в ошибку (если результат recover() был не типа error).
в общем случае это может быть удобно, поскольку паники все равно приходится отлавливать в критически важном коде. особенно если выполняемый ниже код не защищен от паник. (это могут быть сторонние библиотеки, пользовательские функции и пр).
Смотрите, когда вы отлавливаете панику, которую ожидаете, продолжать программу хорошо. Вы её ждали, вы её корректно обработали. Отдыхаем.
С неожиданными паниками же я поддерживаю другой подход. Откуда взялась эта паника нам не известно. Мы понятия не имеем, как её корректно обработать, потому что не знаем её причин. А ведь в нашем сервисе есть состояние. Как мы можем быть уверены, что процесс, вызвавший панику не повредил корректности данных? И если мы бездумно рекаверимся после таких паник, то повреждённое некорректное состояние может породить целую цепочку багов и испорченных данных, и в результате мы обнаружим проблему поздно, когда на её решение понадобится уже гораздо больше ресурсов. Поэтому, лучше иметь один стандартный recover для таких паник, в котором будет только подробная запись в лог и выход в систему с последующим полным перезапуском и анализом ситуации. А для этого и стандартного функционала языка вполне достаточно
Соглашусь с комментатором выше. Я бы предложил определить свой тип для паники и перехватывать в Catch только этот тип
Вы серьезно считаете это упрощением кода, делающим его понятнее?
return try.Val(fn2(try.Val(fn1(...))))
По-моему, вы путаете сокращение и упрощение. Тут надо все эти скобочки рассортировать в голове и не перепутать. При чтении кода на этой строчке все будут тупить и тратить драгоценное время, только чтобы понять, что здесь происходит.
А вот этот код читается легко и непринужденно, вы не задерживаетесь на нём, всё сразу понятно. Дебажить его значительно проще.
a, err := fn1(...)
if err != nil {
// обработка ошибки
}
b, err := fn2(a)
if err != nil {
// обработка ошибки
}
return b
А вот с обработкой внезапной паники, вылетающей из сторонних библиотек, например, полностью поддерживаю!
panic в go существует по большей части для того, чтоб остановить программу с ошибкой. Не для обработки ошибок, а именно программу с ошибкой. То есть кривой код.
Поэтому паника будет при делении на 0, обращении к nil, записи в неинициализированной мапе....
вещи типа regexp.MustCompile существуют, т.к программа не считается "валидной" при не валидном регулярном выражении
По сути это обработка ошибок в коде, которые компилятор не может выявить на этапе компиляции
Ошибки в Go: проблема и элегантное решение с библиотекой try