Прощай error-hell: альтернативная обработка ошибок
- суббота, 24 мая 2025 г. в 00:00:08
В эпоху становления асинхронного программирования JavaScript-разработчики столкнулись с явлением, получившим название "callback-hell" — бесконечной вложенностью функций обратного вызова. Хотя с точки зрения функционального программирования функции являются полноправными гражданами первого класса, принцип "всё хорошо в меру" никто не отменял. Появление Promise и механизма async/await стало спасительным решением этой проблемы.
В мире Go у нас есть более элегантные инструменты — каналы и горутины. Однако совершенству нет предела, и здесь нас поджидает другая ловушка — "error-hell". Новички в Golang часто приходят в недоумение от того, что идиомы языка требуют обработки ошибок буквально на каждом шагу.
У такой педантичности есть неоспоримые преимущества для библиотек общего назначения:
Однако в прикладных программах мы получаем существенное зашумление кода. Передача ошибок вверх по стеку вызовов превращается в "чемодан без ручки" — и тащить тяжело, и выбросить жалко.
Как следствие, в больших проектах на каждом уровне обработки ошибки, по принципу разделения ответственности, может быть добавлена новая запись в лог. В довесок получаем замусоривание логов.
Что если пересмотреть эту практику? Представим себе мир, где мы логируем ошибки в месте их первоначального появления, а передаём наверх только тогда, когда это действительно необходимо для ветвления логики программы.
Но тут возникает закономерный вопрос: как тестировать такой код? Вместо проверки возвращённой ошибки нам нужен способ убедиться, что логирование действительно произошло.
Благодаря механизму структурированного логирования в Go с помощью Slog, записи в лог приобретают формализованную структуру. Это позволяет задавать и выполнять проверки необходимых значений в тестах.
Библиотека comerc/spylog и её аналоги элегантно решают задачу перехвата вывода в лог для целей тестирования.
import (
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type SomeObject struct {
log *slog.Logger // определяем инстанс лога для модуля
val any
}
func NewSomeObject(val any) *SomeObject {
return &SomeObject{
log: slog.With("module", "module_name"), // задаём название модуля для логирования
val: val,
}
}
func (o *SomeObject) SomeMethod() {
// при возникновении ошибки, записываем данные в лог
o.log.Error("test message from some method",
"attr_key1", "attr_val1",
"attr_key2", "attr_val2",
)
}
func TestSomeMethod(t *testing.T) {
var o *SomeObject
logHandler := GetModuleLogHandler("module_name", t.Name(), func() {
o = NewSomeObject("val") // вызываем функцию-конструктор в обёртке logHandler
})
o.SomeMethod() // вызываем тестируемый метод
slog.Error("test message from default") // другие записи в лог не перехватываются
require.True(t, len(logHandler.Records) == 1)
r0 := logHandler.Records[0]
assert.Equal(t, "test message from some method", r0.Message)
assert.Equal(t, "attr_val1", GetAttrValue(r0, "attr_key1"))
assert.Equal(t, "attr_val2", GetAttrValue(r0, "attr_key2"))
}
Данный подход освобождает нас от необходимости совершать грех "shadowed error" или явно игнорировать ошибки. Обработка ошибок по месту их возникновения может значительно облегчить разработку на Go, если руководствоваться здравым смыслом и проводить аналогии с решением callback-hell в JavaScript.
Однако важно помнить, что этот подход требует осознанного применения и может не подходить для всех сценариев использования. Ключ к успеху — в разумном балансе между упрощением кода и сохранением контроля над потоком выполнения программы.
Предложенный подход представляет интересную альтернативу классической обработке ошибок в Go, но требует осторожного применения:
if err != nil
и необходимость передачи ошибок вверх по стеку