Комментарии 19
2016 год. Вам не кажется, что с тех пор обработка ошибок в го поменялась?
Нет. В этом большой плюс языка - он консервативный, знания не устаревают. Со времён Errors.Is/As/Wrap ничего нового не добавили, и я надеюсь, не добавят. Так через 10 лет мой нынешний код будет актуальным, а не дремучим легаси.
Смотря как оценивать. Если код с потенциальными проблемами, сложный к пониманию или избыточный, то можно сказать, что он уже сейчас - дремучее легаси, поэтому через 10 лет хуже уже не станет.
Голосую всеми четырьмя лапами! Go - это матерный язык в мире языков программирования, где простыми выразительными средствами можно сказать больше, чем на своём родном языке.
А как обрабатываются ошибки сейчас?
К сожалению, практически также. Только вместо err.(type) хорошим тоном считается использовать errors.Is() или errors.As().
В чём разница errors.Is()
и errors.As()
package main
import "errors"
type MyError struct {
message string
}
func (e MyError) Error() string {
return e.message
}
func main() {
err := MyError{"My custom error"}
println(errors.Is(err, MyError{"My custom error"})) // true
println(errors.As(err, &MyError{})) // true
}
И fmt.Errorf("could not read config: %w", err)
вместо errors.Wrap(err, "could not read config")
Зачем вообще обрабатывать ошибки? Ошибки - это интерфейс пользователя, а не часть логики. Возникла ошибка - показать пользователю краткое описание + сделать дамп, а дальше служба поддержки разберётся.
Ошибки бывают разные и для некоторых вполне может быть реализована какая то логика обработки.
Как показать пользователю краткое описание, не обработав ошибку?
Дамп чего сделать?
Проблема в том, что некоторые "гении" в виде ошибок возвращают флаги/результат. Например, go-redis возвращает ошибку redis.Nil, если Get не нашел указанный ключ. Т.е. в виде ошибки возвращается валидный результат. Вот и приходится обрабатывать.
Ну не знаю, я вот думаю если там ничего нет я бы лучше ошибку поймал и уже сам решу ошибка или нет, ведь дальше мне надо будет серрилизовать данные, а если там nil у меня все грохнется без проверки, так если проверять я бы лучше проверил на ошибку чем на то что значение nil.
В данном случае, nil - это нормальный ответ от Get, показывающий, что ничего не было найдено. Это не ошибка. Что бы явно обозначить всю дичь такого решения, представьте, что библиотека работы с базой сгенерирует исключение в ответ на какой-нибудь SELECT, если он ничего не нашел. А ещё, что бы ещё сильнее подчеркнуть дичь такого подхода, представьте, что вдруг случится чудо и ошибки в Go станут включать в себя stack trace. Генерация стектрейса - процесс относительно не быстрый. Тут-то возвращение валидного результата в виде ошибки и аукнется. Ну и вообще, просто логически рассуждая, ошибка - это исключительная ситуация. Если это ситуация не исключительная - значит это не ошибка. А ещё можно вспомнить обыкновенный map. Он же не возвращает нам ошибку, если ключ не был там найден. Нет, он возвращает нам флаг, сообщающий об этом.
Какого, пардон, пользователя?
Вот есть процесс, который запускается в фоновом режиме по какому-то событию или по расписанию.
Процесс выбирает из БД по каким-то там условиям 20 000 000 записей и каждую обрабатывает по заданному алгоритму.
9 999 999 записей обработалось нормально. на 10 000 000 записи возникла ошибка. Что делать прикажете? "Интерфейса пользователя" нет - это batch job (фоновое задание). Валить в задание в дамп? А как же оставшиеся 10 000 000 записей? Их кто будет обрабатывать? И когда?
И вот тут должна включаться логика обработки ошибки.
Серьезность ошибки
Возможность продолжения работы с оставшимися данными
Максимально подробное логирование ошибки - где, что и почему случилось
Для всего этого ошибка должна содержать информацию, более полную, нежели "что-то пошло не так". В данном примере - как минимум причина неудачи + ключ записи которую не удалось обработать. Зафиксировали (залогировали) ошибку и пошли дальше. В итоге получим отчет - столько-то записей обработано, такие-то не обработались по таким-то причинам.
На нашей платформе, например, принято использовать т.н. "структурированную ошибку" - 7 символов код + до 5-ти "параметров" по 10 символов каждый. Плюс т.н. "message file" - специальные таблицы с текстовыми расшифровками ошибок и системные API, которые для заданной структурированной ошибки вернут ее полный текст с подстановкой параметров. Это вне зависимости от языка. Это системное.
Естественно, message файлы можно дополнять своими ошибками. При необходимости, ошибки можно складывать в стек ошибок (и на выходе его раскручивать).
Но суть в том, что любая ошибка должна быть обработана и залогирована со всеми подробностями.
Вот ходим в чужую апишку, работает она в целом норм. Но где-то каждый миллионный раз возвращает какую-то 500-ую ошибку. И мы это знаем, и они это знают, но считают это нормальным. Обычный retry вернёт валидный результат. И вот ловлю я эту ошибку, делаю retry чтобы с ней справиться. А мой пользователь, который мне деньги платит, он даже не знает что мы ходим в какую-то внешнюю апишку. И как ему справляться с этим на уровне интерфейса, и чем это лучше моего retry в обработке 500-ой на моей стороне?
"...я не могу понять, откуда взялась исходная ошибка."
И никогда не сможете понять...
Как надо:
Логгер Logrus вместе с текстом пишет имя файла .go, имя функции и номер строки в файле где выводится этот лог - вот так надо делать :-) использовать правильный логгер
Т.е. оборачивать логгером все вызовы функций? =))
Потому как имея
err := MyFunc();
if err != nil {
log(line, err);
}
не скажет вам в каком месте в MyFunc была ошибка если тамMyFunc() {
err := func1()
if err != nil {
return err;
}
err := func2()
if err != nil {
return err;
}
err := func...N()
if err != nil {
return err;
}
}
А в каждой такой функции есть еще функции функции... а там внутри еще и опенсорсные пекеджи...
Что только не сделаешь в отсутствие стэка.
Интерфейс Temporary плохая практика. В исходном коде гошки можно найти комментарий на этот счет
// Deprecated: Temporary errors are not well-defined.
// Most "temporary" errors are timeouts, and the few exceptions are surprising.
// Do not use this method.
source: https://github.com/golang/go/blob/master/src/net/net.go#L397
Идея обрабатывать ошибки не по значению, а по смыслу - правильная, только придется писать именно свою обертку, которая реализует данный функционал. По стандарту в гошке можно полагаться только на Timeout() bool интерфейс
Практика Go — Обработка ошибок (1 часть)