Context - управление временем жизни операций
Представь: твой HTTP-хендлер делает запрос в базу, потом идёт во внешний API. Пользователь закрыл вкладку. Зачем продолжать работу, если результат никому не нужен? Пакет context решает именно эту задачу - позволяет обрубить цепочку операций, когда они больше не нужны.
Зачем нужен context
Без контекста каждая горутина живёт сама по себе. Если главная функция решила, что ответ уже не нужен, фоновые задачи всё равно продолжат работать, тратя CPU и соединения с базой.
Context - это сигнал отмены, который пробрасывается по всей цепочке вызовов: хендлер → сервис → репозиторий → SQL-запрос. Когда контекст отменяется, все звенья цепочки узнают об этом - обычно через канал ctx.Done().
context.Background и context.TODO
Всё начинается с корневого контекста:
ctx := context.Background() // Корневой контекст, точка старта
ctx := context.TODO() // Заглушка - «тут будет контекст, но пока не решил какой»
Background() используется в main(), при инициализации и в тестах. TODO() - временная заглушка, чтобы компилятор не ругался, пока вы рефакторите код.
Отмена по тайм-ауту: WithTimeout
Самый частый сценарий - ограничить время выполнения операции:
package main
import (
"context"
"fmt"
"time"
)
func slowOperation(ctx context.Context) error {
select {
case <-time.After(3 * time.Second): // Операция занимает 3 секунды
fmt.Println("Операция завершена")
return nil
case <-ctx.Done(): // Контекст отменён
return ctx.Err()
}
}
func main() {
// Даём 1 секунду на всё
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // Всегда вызывайте cancel!
err := slowOperation(ctx)
if err != nil {
fmt.Println("Не успели:", err)
// Вывод: Не успели: context deadline exceeded
}
}
WithTimeout возвращает дочерний контекст и функцию cancel. Через 1 секунду канал ctx.Done() закроется, и все, кто его слушают, получат сигнал.
Правило: всегда вызывайте
defer cancel(), даже если тайм-аут сработает сам. Безcancel()ресурсы таймера утекут.
Ручная отмена: WithCancel
Иногда нужно отменить операцию не по времени, а по событию - например, когда получен первый результат из нескольких горутин:
package main
import (
"context"
"fmt"
"time"
)
func search(ctx context.Context, query string, resultCh chan<- string) {
// Имитация поиска с разной скоростью
delay := time.Duration(len(query)) * 100 * time.Millisecond
select {
case <-time.After(delay):
resultCh <- fmt.Sprintf("Результат для '%s'", query)
case <-ctx.Done():
return // Отменили - выходим тихо
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
resultCh := make(chan string, 3)
go search(ctx, "Go", resultCh) // Быстрый
go search(ctx, "Golang", resultCh) // Средний
go search(ctx, "Go context", resultCh) // Медленный
// Берём первый ответ и отменяем остальные
first := <-resultCh
cancel() // Останавливаем остальные горутины
fmt.Println("Первый результат:", first)
}
Дедлайн: WithDeadline
WithDeadline - то же, что WithTimeout, но указывается конкретный момент времени:
// Операция должна завершиться до 15:00
deadline := time.Date(2025, 1, 15, 15, 0, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
На практике WithTimeout используется чаще, потому что обычно думаешь в терминах «дай 5 секунд», а не «сделай до 15:04:32».
Context в HTTP-сервере
Каждый входящий HTTP-запрос уже несёт контекст. Если клиент разорвал соединение, контекст отменяется автоматически:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // Контекст запроса
select {
case <-time.After(5 * time.Second): // Тяжёлая работа
fmt.Fprintln(w, "Готово!")
case <-ctx.Done():
// Клиент ушёл - не тратим ресурсы
log.Println("Клиент отключился:", ctx.Err())
return
}
}
func main() {
http.HandleFunc("/slow", handler)
http.ListenAndServe(":8080", nil)
}
Context в HTTP-клиенте
При исходящих запросах контекст позволяет не зависать, если внешний API лёг:
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", fmt.Errorf("создание запроса: %w", err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("выполнение запроса: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("чтение ответа: %w", err)
}
return string(body), nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := fetchURL(ctx, "https://httpbin.org/delay/10")
if err != nil {
fmt.Println("Ошибка:", err)
// Вывод: Ошибка: выполнение запроса: context deadline exceeded
return
}
fmt.Println(result)
}
Ключевое: используйте http.NewRequestWithContext, а не http.NewRequest + ручное управление. Контекст автоматически прервёт соединение при отмене.
Context в работе с базой данных
Те же принципы работают с database/sql:
func getUser(ctx context.Context, db *sql.DB, id int) (*User, error) {
// Запрос автоматически отменится, если контекст истечёт
row := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id)
var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
return nil, fmt.Errorf("scan user %d: %w", id, err)
}
return &user, nil
}
Все методы database/sql имеют *Context-варианты: QueryContext, ExecContext, QueryRowContext. Используйте именно их.
Передача значений: WithValue
Контекст может нести метаданные - request ID для логов, информацию об авторизации:
package main
import (
"context"
"fmt"
)
// Используем свой тип для ключа, чтобы избежать коллизий
type contextKey string
const requestIDKey contextKey = "requestID"
func processRequest(ctx context.Context) {
reqID, ok := ctx.Value(requestIDKey).(string)
if !ok {
reqID = "unknown"
}
fmt.Printf("[%s] Обработка запроса\n", reqID)
}
func main() {
ctx := context.WithValue(context.Background(), requestIDKey, "req-abc-123")
processRequest(ctx)
}
Антипаттерн: не используйте
WithValueдля передачи обычных аргументов функций. Контекст - для метаданных (request ID, трейсинг, авторизация), а не для бизнес-логики.
Правила работы с context
-
Context - первый параметр функции, всегда с именем
ctx:func DoSomething(ctx context.Context, id int) error { ... } -
Никогда не храните контекст в структуре. Передавайте через аргументы.
-
Всегда вызывайте
cancel()черезdefer- это освобождает ресурсы. -
Не передавайте
nilвместо контекста. Если не знаете, какой контекст нужен, используйтеcontext.TODO(). -
Проверяйте
ctx.Err()в длительных циклах:for _, item := range bigSlice { if ctx.Err() != nil { return ctx.Err() // Быстрый выход } process(item) }
Типичная цепочка в бэкенде
Вот как контекст проходит через типичный HTTP-хендлер:
HTTP запрос (ctx с автоотменой при дисконнекте)
→ handler (добавляет тайм-аут: WithTimeout)
→ service (пробрасывает ctx)
→ repository (QueryContext с этим ctx)
→ PostgreSQL (отменит запрос, если ctx истёк)
Если на любом этапе контекст отменяется, вся цепочка останавливается. Никаких зависших запросов и утечек горутин.
Что запомнить
context.WithTimeout- ограничить время операции (самый частый случай)context.WithCancel- ручная отмена (например, первый результат из нескольких)ctx.Done()- канал, который закрывается при отмене (используйте вselect)ctx.Err()- причина отмены (DeadlineExceededилиCanceled)- Всегда
defer cancel()- даже если тайм-аут сработает сам - Context передаётся первым аргументом, не хранится в структурах
WithValue- только для метаданных, не для бизнес-логики