Pull to refresh

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 существуют, т.к программа не считается "валидной" при не валидном регулярном выражении

По сути это обработка ошибок в коде, которые компилятор не может выявить на этапе компиляции

Sign up to leave a comment.

Articles