← Назад к вопросам

Как устроен контекст?

2.0 Middle🔥 211 комментариев
#Конкурентность и горутины#Основы Go

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Устройство контекста (Context) в Go

Что такое контекст?

Context — это интерфейс в пакете context, который предоставляет способ для отмены операций, установки таймаутов и передачи значений между горутинами и API.

package context

type Context interface {
    // Deadline возвращает время, когда контекст отменится
    Deadline() (deadline time.Time, ok bool)
    
    // Done возвращает канал, закрывающийся при отмене
    Done() <-chan struct{}
    
    // Err возвращает причину отмены (если она произошла)
    Err() error
    
    // Value возвращает значение для ключа
    Value(key interface{}) interface{}
}

Основные типы контекстов

1. Background Context

Корневой контекст, который никогда не отменяется:

ctx := context.Background()
// Используется как основа для других контекстов
// Никогда не отменяется, нет deadline, нет значений

2. TODO Context

Используется, когда точно неизвестен нужный контекст:

ctx := context.TODO()
// Сигнализирует, что контекст ещё не определён
// По сути такой же как Background

3. Context с Timeout/Deadline

WithTimeout — отмена через определённый промежуток времени:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()  // Обязательно отменить контекст

// Контекст отменится через 5 секунд ИЛИ при вызове cancel()
result, err := doSomethingWithTimeout(ctx)

WithDeadline — отмена в конкретное время:

deadline := time.Now().Add(30 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

4. Context с Cancel

WithCancel — отмена вручную:

ctx, cancel := context.WithCancel(context.Background())

go func() {
    // Долгая операция
    time.Sleep(2 * time.Second)
    // Отменить контекст
    cancel()
}()

// Ждём отмены
<-ctx.Done()
fmt.Println("Контекст отменён")

5. Context с Values

WithValue — передача значений:

type RequestID string
const requestIDKey RequestID = "requestID"

ctx := context.WithValue(context.Background(), requestIDKey, "req-123")

// Получить значение
id := ctx.Value(requestIDKey).(string)
fmt.Println("ID запроса:", id)

Практический пример: HTTP запрос с timeout

func fetchUserData(userID int) (*User, error) {
    // Создаём контекст с таймаутом 5 секунд
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    // Создаём HTTP запрос с контекстом
    req, err := http.NewRequestWithContext(ctx, "GET", 
        fmt.Sprintf("https://api.example.com/users/%d", userID), nil)
    if err != nil {
        return nil, err
    }
    
    // Выполняем запрос
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return nil, fmt.Errorf("request timeout")
        }
        return nil, err
    }
    defer resp.Body.Close()
    
    var user User
    json.NewDecoder(resp.Body).Decode(&user)
    return &user, nil
}

Каскадные контексты (Context Tree)

parentCtx, parentCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer parentCancel()

// Дочерний контекст наследует deadline родителя
childCtx, childCancel := context.WithTimeout(parentCtx, 5*time.Second)
defer childCancel()

// childCtx отменится ПЕРВЫМ (его deadline раньше)
// При отмене parentCtx автоматически отменяются все childCtx

Обработка отмены контекста

func doWork(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Работа отменена:", ctx.Err())
            return
        default:
            // Выполняем работу
            fmt.Println("Работаю...")
            time.Sleep(1 * time.Second)
        }
    }
}

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
go doWork(ctx)
defer cancel()
time.Sleep(5 * time.Second)

Лучшие практики

✅ Правильно:

// Передавайте контекст первым параметром
func FetchData(ctx context.Context, id int) (*Data, error) {
    // Используйте ctx для timeout/cancel
    // Проверяйте ctx.Done() в циклах
}

// Не забывайте вызывать cancel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

// Проверяйте ошибку контекста
if ctx.Err() != nil {
    log.Println("Context error:", ctx.Err())
}

❌ Неправильно:

// Не создавайте контекст в функции, если он передан параметром
func Handler(ctx context.Context) {
    newCtx := context.Background()  // ❌ Теряется родительский контекст
    doWork(newCtx)
}

// Не игнорируйте cancel
ctx, _ := context.WithTimeout(...)  // ❌ Утечка ресурсов

// Не используйте контекст для передачи всех значений
ctx = context.WithValue(ctx, "user_db", userDB)  // ❌ Для конфига используй DI

Вывод

Контекст в Go — это элегантный способ управления жизненным циклом операций, установки таймаутов и отмены. Основные операции: timeout, deadline, cancel, values. Всегда проверяйте ctx.Done() и не забывайте вызывать cancel().