Что такое пакет context и почему он важен для управления goroutine?
context.Context несёт дедлайны, отмену и request-scoped значения через весь стек; goroutine обязана проверять ctx.Done() и передавать ctx в каждый внешний вызов — только тогда цепочка отмены работает сквозь db, http и grpc.
Что такое пакет context
Пакет context предоставляет тип context.Context — интерфейс с четырьмя методами: Deadline(), Done(), Err() и Value(). Он переносит сигнал отмены, дедлайны и ограниченное число request-scoped значений вниз по стеку вызовов: через HTTP-обработчики, вызовы базы данных, gRPC-клиенты и фоновые goroutine.
Ключевые конструкторы
context.Background()— корневой контекст, используется в main и тестах.context.WithCancel(parent)— возвращает дочерний ctx и cancel-функцию; отмена распространяется вниз по дереву.context.WithTimeout(parent, d)— автоматически отменяет через d; эквивалентен WithDeadline с time.Now().Add(d).context.WithDeadline(parent, t)— отмена в абсолютный момент времени t.context.WithValue(parent, key, val)— прикрепляет значение; key должен быть unexported типом, чтобы избежать коллизий.
Почему это важно для goroutine
Context сам по себе никого не убивает. Код обязан проверять ctx.Done() и передавать ctx в каждый внешний вызов: db.QueryContext, http.NewRequestWithContext, grpc.DialContext. Только тогда цепочка отмены работает сквозь все слои.
Пример: HTTP-сервер с таймаутом и worker
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"time"
)
func handler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Ограничиваем весь запрос 500 мс
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel() // обязателен: освобождает таймер
var result string
// db.QueryRowContext передаёт ctx в драйвер;
// если таймаут вышел — запрос отменяется на уровне TCP
err := db.QueryRowContext(ctx, "SELECT name FROM users LIMIT 1").Scan(&result)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, result)
}
}
// Worker завершается по отмене контекста
func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case <-ctx.Done():
fmt.Println("worker stopped:", ctx.Err())
return
case j, ok := <-jobs:
if !ok {
return
}
_ = j // обработка задачи
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
jobs := make(chan int, 10)
go worker(ctx, jobs)
// Graceful shutdown: cancel() остановит worker
_ = ctx
}
Правила использования
- Context всегда первым аргументом функции, никогда не в полях структуры.
cancel()всегда черезdefer— даже если контекст истёк раньше, defer освободит ресурсы.context.WithValueтолько для request-scoped данных (trace-id, auth token), не для бизнес-параметров.- Не переиспользовать контекст между запросами: он несёт состояние одного жизненного цикла.
Подводные камни
- Забыть вызвать
cancel()— утечка горутины таймера; особенно опасно в циклах с WithTimeout. - Хранить ctx в поле структуры и обращаться к нему из разных запросов — размытый жизненный цикл.
- Передавать бизнес-данные через
WithValueвместо явных параметров — скрытые зависимости, сложнее тестировать. - Не проверять
ctx.Done()в долгих циклах: отмена не прервёт вычисление автоматически. - Использовать
context.Background()внутри обработчика вместоr.Context()— разрывает цепочку отмены от клиента. - Ключ типа string в WithValue — риск коллизии с другими пакетами; нужен unexported struct{}.
- Не проверять
ctx.Err()после ошибки: нельзя отличить DeadlineExceeded от Canceled без явной проверки. - Слишком широкий таймаут на верхнем уровне без дочерних таймаутов на каждом внешнем вызове — один медленный downstream блокирует весь бюджет.
Common mistakes
- Считать, что создание context автоматически останавливает весь код.
- Использовать context как хранилище зависимостей или глобального состояния.
- Не вызывать cancel у дочерних context с timeout/deadline.
What the interviewer is testing
- Объясняет наследование отмены и дедлайнов.
- Понимает, что cancellation работает только если код слушает Done и передаёт ctx дальше.
- Разделяет request-scoped values и обычные зависимости.