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)

Заключение

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

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

"Человеку свойственно ошибаться; прощать, божественное ». - Александр Поуп