AbortController / AbortSignal → context.Context
Promise.race([fetch(), timeout]) → context.WithTimeout
process.on('SIGTERM', shutdown) → ctx.Done() в select
AsyncLocalStorage → context.WithValue (но осторожно!)
fetch(url, { signal }) → http.NewRequestWithContext(ctx, ...)
ctx.Done() в длительных операцияхmkdir go-context && cd go-context
go mod init go-context
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 1. КОНСТАНТЫ И ТИПЫ ║
// ╚══════════════════════════════════════════════════════════╝
// contextKey — кастомный тип для ключей контекста.
// Использование строк как ключей — плохая практика (коллизии).
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
// Service — имитация сервиса с долгими операциями
type Service struct{}
// ╔══════════════════════════════════════════════════════════╗
// ║ 2. ДЛИТЕЛЬНЫЕ ОПЕРАЦИИ С ПРОВЕРКОЙ КОНТЕКСТА ║
// ╚══════════════════════════════════════════════════════════╝
// LongOperation — операция, которая уважает отмену контекста.
// Периодически проверяет ctx.Done() и прекращает работу.
func (s *Service) LongOperation(ctx context.Context, duration time.Duration) error {
// Создаём тикер для имитации шагов работы
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
steps := int(duration / (100 * time.Millisecond))
completed := 0
for {
select {
case <-ctx.Done():
// Контекст отменён: таймаут, cancel() или дедлайн
fmt.Printf(" [%s] Операция прервана на шаге %d/%d: %v\n",
requestIDFromCtx(ctx), completed, steps, ctx.Err())
return ctx.Err()
case <-ticker.C:
completed++
fmt.Printf(" [%s] Шаг %d/%d выполнен...\n",
requestIDFromCtx(ctx), completed, steps)
if completed >= steps {
fmt.Printf(" [%s] Операция успешно завершена!\n",
requestIDFromCtx(ctx))
return nil
}
}
}
}
// DatabaseQuery — имитация запроса к БД с поддержкой контекста
func (s *Service) DatabaseQuery(ctx context.Context, query string) (string, error) {
// Имитация сетевой задержки
select {
case <-ctx.Done():
return "", fmt.Errorf("query cancelled: %w", ctx.Err())
case <-time.After(500 * time.Millisecond):
return fmt.Sprintf("Результат запроса %q: [{id:1, name:\"Alice\"}]", query), nil
}
}
// HTTPCall — имитация HTTP-запроса с поддержкой контекста
func (s *Service) HTTPCall(ctx context.Context, url string) (string, error) {
// Создаём запрос с контекстом
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", fmt.Errorf("creating request: %w", err)
}
// Выполняем запрос (в реальности — http.DefaultClient.Do(req))
// Здесь имитация с поддержкой отмены
done := make(chan struct {
status int
body string
}, 1)
go func() {
time.Sleep(200 * time.Millisecond) // Имитация задержки сети
done <- struct {
status int
body string
}{200, "OK"}
}()
select {
case <-ctx.Done():
return "", fmt.Errorf("HTTP call cancelled: %w", ctx.Err())
case resp := <-done:
return fmt.Sprintf("HTTP %d: %s", resp.status, resp.body), nil
}
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 3. ХЕЛПЕРЫ ДЛЯ КОНТЕКСТА ║
// ╚══════════════════════════════════════════════════════════╝
// WithRequestID — добавляет request ID в контекст
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
// WithUserID — добавляет user ID в контекст
func WithUserID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
// requestIDFromCtx — извлекает request ID из контекста
func requestIDFromCtx(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return "unknown"
}
// userIDFromCtx — извлекает user ID из контекста
func userIDFromCtx(ctx context.Context) string {
if id, ok := ctx.Value(userIDKey).(string); ok {
return id
}
return "anonymous"
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 4. ДЕМОНСТРАЦИЯ РАЗНЫХ ТИПОВ КОНТЕКСТА ║
// ╚══════════════════════════════════════════════════════════╝
func demoBackground() {
fmt.Println("── 1. CONTEXT.BACKGROUND() ──")
// context.Background() — корневой контекст.
// Используется в main(), при старте сервера, в тестах.
// Он НИКОГДА не отменяется, не имеет дедлайна и значений.
ctx := context.Background()
fmt.Printf(" Background: %v\n", ctx)
fmt.Printf(" Err: %v\n\n", ctx.Err())
}
func demoWithCancel() {
fmt.Println("── 2. CONTEXT.WITHCANCEL() ──")
// WithCancel создаёт контекст, который можно отменить вручную.
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Всегда вызывай cancel() для освобождения ресурсов!
svc := &Service{}
// Запускаем долгую операцию в горутине
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
ctx := WithRequestID(ctx, "req-cancel-001")
err := svc.LongOperation(ctx, 1*time.Second)
if err != nil {
fmt.Printf(" Результат: %v\n", err)
}
}()
// Отменяем через 300ms
time.Sleep(300 * time.Millisecond)
fmt.Println(" ▶ Вызываем cancel()...")
cancel()
wg.Wait()
fmt.Println()
}
func demoWithTimeout() {
fmt.Println("── 3. CONTEXT.WITHTIMEOUT() ──")
// WithTimeout автоматически отменяет контекст через заданное время.
// Аналог: Promise.race([task, timeout(500ms)])
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() // Важно! Освобождает таймер.
svc := &Service{}
ctx = WithRequestID(ctx, "req-timeout-001")
// Операция на 1 секунду — должна прерваться через 500ms
start := time.Now()
err := svc.LongOperation(ctx, 1*time.Second)
elapsed := time.Since(start)
fmt.Printf(" Прошло: %v\n", elapsed)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Printf(" ✓ Контекст отменён по таймауту: %v\n", err)
}
fmt.Println()
}
func demoWithDeadline() {
fmt.Println("── 4. CONTEXT.WITHDEADLINE() ──")
// WithDeadline — как WithTimeout, но с абсолютным временем.
deadline := time.Now().Add(300 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
svc := &Service{}
ctx = WithRequestID(ctx, "req-deadline-001")
start := time.Now()
err := svc.LongOperation(ctx, 1*time.Second)
elapsed := time.Since(start)
fmt.Printf(" Прошло: %v\n", elapsed)
if err != nil {
fmt.Printf(" Операция прервана: %v\n", err)
}
fmt.Println()
}
func demoWithValue() {
fmt.Println("── 5. CONTEXT.WITHVALUE() ──")
// WithValue добавляет ключ-значение в контекст.
// Используй ТОЛЬКО для request-scoped данных:
// request ID, trace ID, user info, логгер.
// НЕ используй для передачи бизнес-параметров!
ctx := context.Background()
ctx = WithRequestID(ctx, "req-value-001")
ctx = WithUserID(ctx, "user-42")
// Извлечение значений
fmt.Printf(" Request ID: %s\n", requestIDFromCtx(ctx))
fmt.Printf(" User ID: %s\n", userIDFromCtx(ctx))
fmt.Printf(" Missing key: %v\n", ctx.Value("missing")) // nil
// Передача контекста в сервис
svc := &Service{}
ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
defer cancel()
result, err := svc.DatabaseQuery(ctx, "SELECT * FROM users")
if err != nil {
fmt.Printf(" Ошибка запроса: %v\n", err)
} else {
fmt.Printf(" Результат: %s\n", result)
}
fmt.Println()
}
func demoHTTPWithContext() {
fmt.Println("── 6. HTTP С КОНТЕКСТОМ ──")
svc := &Service{}
// Запрос с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
ctx = WithRequestID(ctx, "req-http-001")
fmt.Println(" Отправляем HTTP-запрос с таймаутом 100ms...")
result, err := svc.HTTPCall(ctx, "https://api.example.com/users")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Printf(" ✓ Таймаут HTTP-запроса: %v\n", err)
} else {
fmt.Printf(" Ошибка: %v\n", err)
}
} else {
fmt.Printf(" Результат: %s\n", result)
}
// Успешный запрос
ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel2()
ctx2 = WithRequestID(ctx2, "req-http-002")
fmt.Println(" Отправляем HTTP-запрос с таймаутом 500ms...")
result2, err2 := svc.HTTPCall(ctx2, "https://api.example.com/users")
if err2 != nil {
fmt.Printf(" Ошибка: %v\n", err2)
} else {
fmt.Printf(" Результат: %s\n", result2)
}
fmt.Println()
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 5. GRACEFUL SHUTDOWN С КОНТЕКСТОМ ║
// ╚══════════════════════════════════════════════════════════╝
func demoGracefulShutdown() {
fmt.Println("── 7. GRACEFUL SHUTDOWN ──")
fmt.Println(" (Нажмите Ctrl+C для демонстрации...)")
// Создаём контекст, который отменится при получении сигнала
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt, // Ctrl+C
syscall.SIGTERM, // kill
)
defer stop()
svc := &Service{}
ctx = WithRequestID(ctx, "server-main")
// Имитация работы сервера
fmt.Println(" Сервер запущен. Ожидание сигнала...")
fmt.Println(" Доступные действия:")
fmt.Println(" 1. Нажмите Ctrl+C для graceful shutdown")
fmt.Println(" 2. Подождите 5 секунд для автоматического завершения")
// Запускаем фоновую задачу с под-контекстом
taskCtx, taskCancel := context.WithTimeout(ctx, 10*time.Second)
defer taskCancel()
go func() {
err := svc.LongOperation(taskCtx, 10*time.Second)
if err != nil {
fmt.Printf("\n Фоновая задача: %v\n", err)
}
}()
// Ждём сигнал или таймаут
select {
case <-ctx.Done():
fmt.Printf("\n Получен сигнал: %v\n", ctx.Err())
fmt.Println(" Выполняем graceful shutdown...")
// Здесь: закрываем соединения, ждём завершения запросов
time.Sleep(500 * time.Millisecond) // Имитация очистки
fmt.Println(" Сервер остановлен.")
case <-time.After(5 * time.Second):
fmt.Println("\n Демонстрация завершена (5 секунд).")
}
fmt.Println()
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 6. ПАТТЕРНЫ ПРАВИЛЬНОГО ИСПОЛЬЗОВАНИЯ КОНТЕКСТА ║
// ╚══════════════════════════════════════════════════════════╝
func demoBestPractices() {
fmt.Println("── 8. BEST PRACTICES ──")
fmt.Println(" ✓ Контекст всегда первый параметр: func(ctx context.Context, ...)")
fmt.Println(" ✓ Не храни контекст в структуре — передавай явно")
fmt.Println(" ✓ context.Background() только в main/test/init")
fmt.Println(" ✓ Всегда вызывай cancel() для WithCancel/Timeout/Deadline")
fmt.Println(" ✓ Используй WithValue только для request-scoped данных")
fmt.Println(" ✓ Используй кастомные типы для ключей (не строки)")
fmt.Println(" ✓ Проверяй ctx.Done() в длительных циклах")
fmt.Println(" ✓ Передавай контекст через ВСЕ слои: handler → service → repository")
fmt.Println(" ✗ Не передавай nil-контекст (если не уверен — context.TODO())")
fmt.Println(" ✗ Не используй WithValue для бизнес-логики")
fmt.Println(" ✗ Не игнорируй отмену контекста")
fmt.Println()
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 7. ЦЕПОЧКА КОНТЕКСТОВ (демонстрация) ║
// ╚══════════════════════════════════════════════════════════╝
func demoContextChain() {
fmt.Println("── 9. ЦЕПОЧКА КОНТЕКСТОВ ──")
// Контексты образуют дерево. Отмена родителя отменяет всех потомков.
parent, parentCancel := context.WithCancel(context.Background())
defer parentCancel()
child, childCancel := context.WithTimeout(parent, 500*time.Millisecond)
defer childCancel()
grandchild, grandchildCancel := context.WithCancel(child)
defer grandchildCancel()
fmt.Printf(" Parent: %v\n", parent)
fmt.Printf(" Child: %v\n", child)
fmt.Printf(" Grandchild: %v\n", grandchild)
// Проверяем цепочку отмены
go func() {
<-grandchild.Done()
fmt.Printf(" Grandchild отменён: %v\n", grandchild.Err())
}()
go func() {
<-child.Done()
fmt.Printf(" Child отменён: %v\n", child.Err())
}()
// Отменяем parent — все потомки тоже отменятся
time.Sleep(100 * time.Millisecond)
fmt.Println(" ▶ Отменяем parent...")
parentCancel()
time.Sleep(100 * time.Millisecond)
fmt.Println()
}
// ╔══════════════════════════════════════════════════════════╗
// ║ MAIN ║
// ╚══════════════════════════════════════════════════════════╝
func main() {
fmt.Println("╔══════════════════════════════════════════╗")
fmt.Println("║ КОНТЕКСТ: ОТМЕНА, ДЕДЛАЙНЫ, ЗНАЧЕНИЯ ║")
fmt.Println("╚══════════════════════════════════════════╝")
demoBackground()
demoWithCancel()
demoWithTimeout()
demoWithDeadline()
demoWithValue()
demoHTTPWithContext()
demoBestPractices()
demoContextChain()
demoGracefulShutdown()
fmt.Println("✅ Демонстрация завершена!")
}
context.Background() ← корень (никогда не отменяется) │ ├── WithCancel() │ │ │ ├── WithTimeout(5s) │ │ │ │ │ └── WithValue(“requestID”, “123”) │ │ │ └── WithDeadline(2024-01-01T00:00:00Z) │ └── WithCancel() ← для graceful shutdown │ └── signal.NotifyContext(ctx, os.Interrupt)
При отмене родителя: ┌────────────────────────────────────────────────────────┐ │ Parent cancel() → Child.Done() → Grandchild.Done() │ │ Сигнал распространяется ВНИЗ по дереву. │ │ Дочерние контексты НЕ могут отменить родительские. │ └────────────────────────────────────────────────────────┘
| Функция | Назначение | Когда использовать |
|---|---|---|
context.Background() | Корневой контекст | main(), init(), тесты |
context.TODO() | Заглушка “я ещё не знаю, какой контекст” | Рефакторинг, прототипы |
context.WithCancel(parent) | Ручная отмена | Graceful shutdown, прерывание операций |
context.WithTimeout(parent, d) | Отмена через время | HTTP-запросы, запросы к БД |
context.WithDeadline(parent, t) | Отмена к точному времени | Запланированные операции |
context.WithValue(parent, key, val) | Передача значений | Request ID, trace ID, user info |
# Запуск (будет ждать Ctrl+C в конце)
go run main.go
# Сборка
go build -o context-demo main.go
./context-demo
💡 Best practices от сеньоров:
func Do(ctx context.Context, arg string) error
💡 Для Node.js разработчика:
ctx.Done() — канал, который закрывается при отмене. Аналог signal.aborted + signal.addEventListener('abort').