6 проблем, связанных с ошибками, которые я пытаюсь решить
Я люблю Go и пишу на нем код почти десять лет, но исправление ошибок всегда было проблемой. Моя предыдущая компания была одной из первых компаний в мире, использовавших Golang в производстве, и я с самого первого дня боролся с тем, как лучше всего справляться с ошибками. Почему-то вещи никогда не казались правильными, поэтому в последнее время я начал по-настоящему экспериментировать с ними, чтобы заставить их использовать их.
Вот проблемы, которые я хотел решить:
- Как мне получить трассировку стека, где произошла ошибка?
- Когда и где мне следует писать журнал ошибок?
- Как отличить ошибку, которую я хочу показать пользователю, и то, что я хочу отображать в журналах?
- Как мне добавить контекст, чтобы я мог использовать структурированное ведение журнала, когда я хочу регистрировать ошибку?
- Как уменьшить объем кода, предназначенного для обработки ошибок и ведения журнала?
- Как мне следовать общепринятым передовым методикам Go, например, предложенным Дэйвом Чейни, и выполнять все вышеперечисленное?
Я все перепробовала хотя бы раз, просто чтобы посмотреть, каково это.
Например, ведение журнала в самой глубокой точке, чтобы я мог получить трассировку стека в своих журналах и надлежащий контекст, а затем возвращать ошибку. Библиотеки журналов, такие как Uber’s Zap, будут печатать трассировку стека и структурированный контент:
Мне это всегда казалось немного некрасивым, так как каждый раз, когда вы получаете сообщение об ошибке, требуется дополнительная строка для регистрации. Это также означает передачу logger
, либо явно добавляя его в качестве параметра к каждой функции, либо используя контекст для его передачи (более чистый способ). Некоторые могут поспорить, что добавлять это в контекст - тоже не лучшая идея.
И вы неизбежно регистрируете одну и ту же ошибку более одного раза, если не будете осторожны и не убедитесь, что она еще не была зарегистрирована глубже. В итоге получаются такие вещи:
Вы видите, к чему все идет, и это не самое удачное место!
Когда вы работаете над API (а это, вероятно, большинство из нас), ситуация может стать еще хуже с таким кодом:
Теперь это становится действительно ужасно. И весь этот лишний плохой код повсюду при каждой проверке ошибок (а в Go у вас есть проверки на ошибки повсюду).
Так что же делать плохому разработчику Go?
Мои последние эксперименты с ошибками Go
Думаю, я наконец-то нашел довольно элегантный способ справиться со всем, что описано выше, при этом сводя код обработки ошибок к минимуму. Откровение в моем собственном сознании.
У меня есть несколько репозиториев GitHub, которые я использую для экспериментов, и в этом разделе я буду ссылаться на один, в частности, под названием gotils.
Шаг 1. Добавьте контекст в контекст
Вместо добавления контекста в ваши logger
(структурированные поля) добавьте их в контекст Go:
ctx = gotils.With(ctx, "foo", "bar")
Приятно то, что вы можете использовать эти контекстные поля где угодно, а не только для своей библиотеки журналов. Это просто базовая карта записей. И добавление контекста к контексту кажется подходящим вариантом.
Шаг 2. Добавьте контекст и трассировку стека к ошибкам
Вместо того, чтобы пытаться войти в глубину, чтобы мы могли получить правильную трассировку стека и контекст, давайте добавим стек и контекст к возвращаемым ошибкам:
if err != nil { return gotils.C(ctx).Errorf("error on x: %v", err) }
Функция gotils.Errorf
обертывает вашу ошибку и добавляет текущий стек вместе с картой поля контекста и возвращает новую ошибку с этой информацией.
Шаг 3. Зарегистрируйте и верните пользователю в точке входа ответ об ошибке.
Если это API, и мы получили ошибку, то мы будем обрабатывать ошибку только в точке входа (то есть в обработчике HTTP):
Итак, теперь мы удалили весь уродливый код обработки ошибок и журналирования изнутри приложения и делаем это только один раз на грани .
Еще больше сократите ведение журнала и обработку ошибок
Чтобы сделать этот тип обработки еще лучше, попробуйте использовать ErrorHandler
, который позволяет вашим обработчикам HTTP возвращать ошибку. Это означает, что нам не нужно иметь дело с ошибками в каждом обработчике - достаточно одного места, в котором хранятся все остальные обработчики. Это выглядит так:
Тогда у вас есть одно место во всем приложении, которое занимается регистрацией и возвратом ошибок.
Вы можете увидеть полный пример ErrorHandler
, который касается UserErrors
и прочего.
Пользовательские ошибки, чтобы скрыть внутренние ошибки от пользователей и вернуть информативные сообщения
Часто вы не хотите, чтобы пользователи видели ошибку, возникшую в вашей программе (например, ошибку базы данных). Вероятно, вы не захотите отправлять sql.ErrTxDone
обратно своему пользователю, но вы, вероятно, захотите зарегистрировать его или как-то с этим справиться.
Один из способов справиться с этим - просто вернуть общие ошибки, такие как внутренняя ошибка сервера 500, если ошибка устраняется, но это очень жестко, и вы не можете вернуть пользователю подробный ответ о том, что именно пошло не так. Например, если это просто недопустимый ввод, вы должны вернуть сообщение типа «поле X недействительно».
Итак, нам нужен способ различать сообщения об ошибках для внутреннего использования и сообщения об ошибках для пользователей. Для этого я сделал UserError
интерфейс:
type UserError interface { error UserError() string }
Он используется так:
return gotils.UserErrorf(err, "field %v is invalid", fieldname)
Затем, когда пришло время ответить:
Также есть gotils.HTTPError
, если вы хотите вернуть определенные коды статуса HTTP.
Как использовать их с вашей библиотекой журналов
Ваша библиотека журналов не знает, как автоматически извлекать стек или поля, но, к счастью, это довольно просто. Более того, у вас может быть только одна строка во всем приложении, которая фактически использует вашу библиотеку ведения журнала!
Если вы попробуете описанное выше со своим собственным кодом, просто убедитесь, что у вас есть несколько методов для получения полей контекста и стека из ошибки. gotils
имеет интерфейсы Stacked
и Fielded
, которые предоставляют эти методы, которые вы можете скопировать или просто использовать напрямую, если хотите:
Позвоните в них, чтобы получить нужную информацию и передать ее в свою библиотеку журналов.
Пример этого в репозитории gcputils, который будет регистрировать все это в правильном формате для ведения журнала Google Cloud с помощью простого вызова:
gcputils.Printf("%v", err)
Заключение
Эти вещи позволили мне очистить много беспорядочного кода, который всегда меня беспокоил, но я был слишком занят (или ленив), чтобы придумать лучший способ. То есть до недавнего времени, когда у меня было достаточно времени, чтобы поэкспериментировать с ведением журнала и обработкой ошибок.
Попробуйте некоторые из обсуждаемых здесь концепций и посмотрите, что вы думаете. Я хотел бы услышать ваши отзывы или узнать, как вы справляетесь с этими проблемами.
"Человеку свойственно ошибаться; прощать, божественное ». - Александр Поуп