Урок 9: Контекст — отмена, дедлайны, значения

Урок 9. Контекст — отмена, дедлайны, значения

🔄 Node.js → Go (ключевые аналоги):

📋 Что изучаем

📦 Инициализация проекта

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 от сеньоров:

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →