Код-ревью Middle context спрашивают часто обновлено

Код-ревью Go: context в HTTP-обработчике

Вопрос

Обработчик читает заказы из базы и пишет аудит. Найдите ошибки работы с context и ресурсами.

type Service struct {
    ctx context.Context
    db  *sql.DB
}

func (s *Service) Handle(w http.ResponseWriter, r *http.Request) {
    ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
    ctx = context.WithValue(ctx, "user", r.Header.Get("X-User"))
    rows, err := s.db.QueryContext(ctx, "SELECT id FROM orders")
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    go s.audit(r.Context(), "orders viewed")
    writeRows(w, rows)
}

Коротко

Исправленный обработчик: Пользователь берётся из контекста, который заполнил middleware авторизации (auth.UserID с ключом своего типа).

Что должен найти ревьюер

  1. Стильстрока 2
    Контекст хранится в поле структуры: он относится к одному запросу или операции, его передают первым аргументом.
  2. Ошибкастрока 7
    cancel выброшен: таймер и ресурсы контекста живут до истечения 5 секунд после каждого запроса. go vet (lostcancel) это ловит. Нужно defer cancel().
  3. Ошибкастрока 7
    Родитель Background вместо r.Context(): если клиент отключился, запрос к базе продолжит выполняться впустую.
  4. Стильстрока 8
    Ключ строкой может совпасть с ключом другого пакета. Нужен свой неэкспортируемый тип ключа. Кроме того, пользователя стоит класть в контекст в middleware авторизации, а не в каждом обработчике.
  5. Безопасностьстрока 8
    Личность пользователя берётся из заголовка, который клиент может подставить сам. Её должна определять авторизация (токен, сессия).
  6. Безопасностьстрока 11
    Текст внутренней ошибки базы уходит клиенту: утечка деталей схемы и инфраструктуры. Клиенту общий текст, подробности в лог.
  7. Ошибкастрока 9
    rows не закрываются (defer rows.Close()): соединение не вернётся в пул, а ошибка rows.Err() не проверяется.
  8. Ошибкастрока 14
    Контекст запроса отменяется, как только обработчик вернётся, поэтому запись аудита будет обрываться. Нужен context.WithoutCancel(r.Context()) с собственным таймаутом.

Разбор

Исправленный обработчик:

func (s *Service) Handle(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()
    rows, err := s.db.QueryContext(ctx, "SELECT id FROM orders WHERE user_id = $1", auth.UserID(ctx))
    if err != nil {
        slog.ErrorContext(ctx, "query orders", "err", err)
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    bg, bgCancel := context.WithTimeout(context.WithoutCancel(r.Context()), 3*time.Second)
    go func() {
        defer bgCancel()
        s.audit(bg, "orders viewed")
    }()
    writeRows(w, rows)
}

Пользователь берётся из контекста, который заполнил middleware авторизации (auth.UserID с ключом своего типа).

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.