В эпоху становления асинхронного программирования 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.
Однако важно помнить, что этот подход требует осознанного применения и может не подходить для всех сценариев использования. Ключ к успеху — в разумном балансе между упрощением кода и сохранением контроля над потоком выполнения программы.
Анализ от Claude
Предложенный подход представляет интересную альтернативу классической обработке ошибок в Go, но требует осторожного применения:
Сильные стороны
- Убираем шаблонный
if err != nil
и необходимость передачи ошибок вверх по стеку - Логирование в месте возникновения ошибки может быть очень полезным для отладки
- Элегантное решение для тестирования через перехват логов
Потенциальные риски
- Потеря семантики — вызывающий код теряет информацию о том, что операция завершилась неудачно
- Нарушение контракта — функция может "молча" провалиться, что противоречит принципу явности в Go
- Неопределенность состояния — функция может завершиться "успешно", но с внутренними ошибками
- Сложность композиции — труднее строить сложную логику, когда неясно, какие операции успешны
- Сложность отладки — без явного возврата ошибок труднее отследить цепочку проблем и путь их распространения
Рекомендации по применению
- Подходит для утилитарных функций, где ошибка не влияет на основной поток
- Хорошо работает в сценариях "best effort" (например, метрики, аналитика)
- Не рекомендуется для критически важных операций (работа с БД, файлами, сетью)