Как устроены контексты в Go
В Go context — это стандартный механизм управления жизненным циклом операций, особенно в многопоточных и сетевых приложениях. Он используется для передачи сигналов отмены, дедлайнов (сроков выполнения) и дополнительных данных (значений) между различными частями программы, особенно между функциями, горутинами и обработчиками.
Контексты реализованы через интерфейс context.Context, определённый в стандартной библиотеке context.
1. Интерфейс context.Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Каждый контекст представляет собой неизменяемый объект, из которого можно получить:
-
Deadline() — возвращает время, к которому должна завершиться операция.
-
Done() — канал, закрывающийся при отмене или по дедлайну.
-
Err() — возвращает причину отмены (context.Canceled, context.DeadlineExceeded).
-
Value(key) — позволяет передавать дополнительные значения.
2. Базовый контекст: context.Background и context.TODO
context.Background()
Это пустой корневой контекст, с которого начинают построение цепочек:
ctx := context.Background()
Он никогда не отменяется, не содержит дедлайна и значений. Используется как корень дерева контекстов.
context.TODO()
Используется как временный контекст-заглушка, если ещё не решено, какой контекст использовать:
ctx := context.TODO()
3. Создание новых контекстов
Новые контексты создаются на основе уже существующих с помощью обёрток. Все они возвращают:
-
новый Context;
-
CancelFunc, которую нужно вызвать для завершения контекста вручную (если применимо).
context.WithCancel(parent Context)
Создаёт контекст, который можно отменить вручную:
ctx, cancel := context.WithCancel(parent)
defer cancel()
Когда вызывается cancel(), закрывается канал Done(), и все дочерние контексты тоже отменяются.
context.WithTimeout(parent Context, timeout time.Duration)
Создаёт контекст, автоматически отменяемый через заданное время:
ctx, cancel := context.WithTimeout(context.Background(), 2\*time.Second)
defer cancel()
Если операция не завершена за 2 секунды — контекст отменяется, и ctx.Err() будет context.DeadlineExceeded.
context.WithDeadline(parent Context, t time.Time)
Аналогичен WithTimeout, но с указанием конкретного времени:
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1\*time.Second))
defer cancel()
context.WithValue(parent Context, key, value any)
Добавляет значение в контекст:
ctx := context.WithValue(context.Background(), "userID", 42)
Значения предназначены для сквозной передачи параметров, например, ID пользователя, токенов, trace-идентификаторов. Но не рекомендуется использовать для передачи данных, которые лучше передавать явно.
4. Канал Done()
Канал Done() — главное средство контроля. Он закрывается, когда:
-
контекст отменён вручную (cancel()),
-
истёк дедлайн (timeout или deadline),
-
отменён родительский контекст.
Пример:
select {
case <-ctx.Done():
fmt.Println("Операция отменена:", ctx.Err())
case result := <-someChan:
fmt.Println("Результат:", result)
}
5. Контекст и горутины
Контекст особенно полезен для управления временем жизни горутин:
func doSomething(ctx context.Context) {
go func() {
select {
case <-ctx.Done():
fmt.Println("Горутина остановлена:", ctx.Err())
return
case <-time.After(5 \* time.Second):
fmt.Println("Работа завершена")
}
}()
}
Если вызвать cancel(), горутина завершится досрочно.
6. Цепочка контекстов
Контексты образуют дерево: каждый контекст знает своего родителя. При отмене родителя — отменяются все потомки.
root := context.Background()
ctx1, cancel1 := context.WithCancel(root)
ctx2, cancel2 := context.WithTimeout(ctx1, 2\*time.Second)
ctx3 := context.WithValue(ctx2, "requestID", "abc123")
Если вызвать cancel1(), ctx2 и ctx3 также отменятся.
7. Типичные ошибки при работе с context
- Забытая cancel(): приводит к утечкам памяти (особенно с WithTimeout, WithCancel, WithDeadline).
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // обязательно!
-
Передача контекста через значение (ctx := ctx): контексты копируются по значению, но это безопасно, так как они содержат указатели внутри.
-
Чрезмерное использование WithValue: рекомендуется использовать только для данных, общих для всей цепочки запроса, например, request ID.
8. Контекст в HTTP и gRPC
HTTP
В net/http каждый запрос сопровождается контекстом:
func handler(w http.ResponseWriter, r \*http.Request) {
ctx := r.Context()
select {
case <-ctx.Done():
log.Println("Клиент отменил запрос")
return
case <-time.After(3 \* time.Second):
w.Write(\[\]byte("Готово"))
}
}
Если клиент закроет соединение, сервер узнает об этом через ctx.Done().
gRPC
Контекст передаётся и используется аналогично:
func (s \*MyService) Do(ctx context.Context, req \*Request) (\*Response, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return &Response{}, nil
}
}
9. Пользовательские ключи для WithValue
Ключи для WithValue не должны быть строками (во избежание конфликтов). Рекомендуется использовать собственные типы:
type ctxKey string
const userKey ctxKey = "userID"
ctx := context.WithValue(context.Background(), userKey, 42)
val := ctx.Value(userKey).(int)
10. Отмена снаружи: таймер/сигналы
Можно использовать context.WithCancel и отменять по сигналу ОС:
ctx, cancel := context.WithCancel(context.Background())
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
cancel()
}()
Это позволяет завершать все операции при прерывании процесса.
11. Контекст в базах данных
Библиотеки SQL (например, database/sql) и драйверы поддерживают context:
ctx, cancel := context.WithTimeout(context.Background(), 2\*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", 1)
Если операция зависнет — контекст прервёт её.
12. Работа с дочерними горутинами
Контексты особенно полезны для управления группами горутин:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5\*time.Second)
defer cancel()
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Println("Горутина", i, "остановлена")
case <-time.After(time.Duration(i) \* time.Second):
fmt.Println("Горутина", i, "завершилась")
}
}(i)
}
wg.Wait()
}
13. Влияние на производительность
Контексты лёгкие (всего несколько указателей), дешёвы в копировании и безопасны для многопоточности. Они не создают отдельной горутины (если только вы сами её не запускаете при WithCancel и т.д.), а используют внутренние структуры для управления отменой.
14. Контексты и профилирование
Контексты могут быть расширены профилировщиками, трассировкой (net/http/pprof, go tool trace), и использоваться в инструментах observability (OpenTelemetry). Они позволяют передавать trace-id, request-id, user-id сквозь сервисы.