Как устроены контексты в 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 сквозь сервисы.