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