🕳 context.
Мы передаем ctx context.Context первым аргументом почти в каждую функцию. Это кровеносная система Go-приложений, которая отлично справляется с отменой операций и таймаутами.
Но есть в интерфейсе контекста один метод, который открывает портал в ад - это Value().
Часто разработчики (особенно выходцы из языков с thread-local storage) смотрят на ctx.Value и думают: "О, отличная глобальная мапа! Положу-ка я сюда инстанс базы данных, логгер и данные пользователя, чтобы не прокидывать их через аргументы 10 функций".
Давайте разберем, почему это архитектурное преступление.
❌ Проблема 1: Убийство статической типизации
Сила Go - в строгой типизации на этапе компиляции. Когда вы кладете что-то в контекст, оно превращается в any (или interface{}).
// Где-то в мидлваре
ctx = context.WithValue(ctx, "db", dbConnection)
// Где-то в репозитории
db := ctx.Value("db").(*sql.DB) // Молимся, чтобы там не было nil
Ваша функция теперь имеет скрытую зависимость. Глядя на сигнатуру func GetUser(ctx context.Context), невозможно понять, что для ее работы нужен коннект к БД. Узнаете вы об этом только в рантайме, когда словите panic: interface conversion.
❌ Проблема 2: Медленный поиск (O(N))
Контекст - это не map (хэш-таблица). Под капотом WithValue каждый раз создает новый узел, который ссылается на родительский контекст. Образуется связный список (дерево).
Когда вы вызываете ctx.Value("key"), Go берет текущий узел и проверяет ключ. Если не нашел — идет к родителю. И так до самого верха. Если ваш ключ лежит в самом начале цепочки из 20 мидлварей, поиск будет прочесывать память каждый раз, убивая кэш процессора.
Как использовать ctx.Value правильно?
Официальная документация гласит: "Используйте значения контекста только для данных, привязанных к области видимости запроса (request-scoped data)".
✅ Идеальные кандидаты для ctx.Value:
• TraceID / RequestID (для распределенного трейсинга).
• IP-адрес клиента.
• ID авторизованного пользователя (но не вся структура User с бизнес-логикой).
То есть данные, которые нужны инфраструктуре (логгеру, метрикам), но никак не влияют на бизнес-логику функции.
🔥 Защита от коллизий ключей
Никогда не используйте встроенные типы (например, string) в качестве ключей для WithValue. Если два разных пакета используют ключ "id", они перезапишут данные друг друга.
Всегда создавайте неэкспортируемый кастомный тип:
type contextKey string
const userIDKey contextKey = "user_id"
// Обертка для записи
func WithUserID(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
// Обертка для чтения (безопасная, возвращает (int, bool))
func UserIDFromContext(ctx context.Context) (int, bool) {
id, ok := ctx.Value(userIDKey).(int)
return id, ok
}
Про context.TODO():
Если вы пишете код и не знаете, откуда взять контекст (например, рефакторите старый легаси) - используйте context.TODO(). Технически это тот же context.Background(), но семантически это маячок для линтеров и коллег: "Я оставил здесь технический долг, позже нужно прокинуть нормальный контекст".
#golang #architecture #context #bestpractices #cleancode
👉 @golang_lib