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

Как написать context.Timeout?

1.0 Junior🔥 252 комментариев
#Основы Go

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

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

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

Использование context.WithTimeout

context.WithTimeout — ключевой механизм для установки дедлайнов в Go-приложениях. Он создает контекст, который автоматически отменяется по истечении заданного времени, что критически важно для предотвращения "подвешивания" операций и управления ресурсами.

Базовый синтаксис

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Создаем контекст с таймаутом 2 секунды
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel() // Важно: освобождаем ресурсы

    // Передаем контекст в функцию
    doWork(ctx)
}

func doWork(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("Работа завершена")
    case <-ctx.Done():
        fmt.Println("Отменено по таймауту:", ctx.Err())
    }
}

Практические паттерны использования

1. HTTP-запросы с таймаутом

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()
    
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return "", err
    }
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    return string(body), err
}

2. База данных и внешние сервисы

func queryDatabase(ctx context.Context, query string) ([]Result, error) {
    // Устанавливаем таймаут для конкретной операции
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    // Контекст передается в драйвер БД
    rows, err := db.QueryContext(ctx, query)
    if err != nil {
        return nil, fmt.Errorf("ошибка запроса: %w", err)
    }
    defer rows.Close()
    
    // Обработка результатов
    var results []Result
    for rows.Next() {
        // ...
    }
    return results, rows.Err()
}

3. Каскадирование таймаутов

func processWithCascadingTimeouts() error {
    // Родительский контекст с общим таймаутом
    parentCtx, parentCancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer parentCancel()
    
    // Дочерние контексты с более короткими таймаутами
    dataCtx, dataCancel := context.WithTimeout(parentCtx, 3*time.Second)
    defer dataCancel()
    
    logCtx, logCancel := context.WithTimeout(parentCtx, 1*time.Second)
    defer logCancel()
    
    // Параллельное выполнение с разными таймаутами
    var wg sync.WaitGroup
    wg.Add(2)
    
    go fetchData(dataCtx, &wg)
    go writeLog(logCtx, &wg)
    
    wg.Wait()
    return nil
}

Критически важные аспекты

Всегда вызывайте cancel()

  • Функция cancel освобождает ресурсы контекста
  • Используйте defer cancel() сразу после создания контекста
  • Невызов cancel может привести к утечкам памяти
// ПРАВИЛЬНО:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

// ОПАСНО (потенциальная утечка):
ctx, _ := context.WithTimeout(context.Background(), time.Second)

Проверка типа ошибки

func handleOperation(ctx context.Context) error {
    resultChan := make(chan Result)
    go performTask(resultChan)
    
    select {
    case res := <-resultChan:
        return res
    case <-ctx.Done():
        // Анализ причины отмены
        switch ctx.Err() {
        case context.DeadlineExceeded:
            return fmt.Errorf("превышен дедлайн")
        case context.Canceled:
            return fmt.Errorf("операция отменена")
        default:
            return fmt.Errorf("неизвестная ошибка: %w", ctx.Err())
        }
    }
}

Распространенные ошибки

  1. Переиспользование отмененного контекста

    // Неправильно
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    cancel()
    // ctx теперь отменен - не используйте его для новых операций
    
  2. Игнорирование контекста в горутинах

    // Опасный код
    go func() {
        // Если ctx отменяется, эта горутина может зависнуть
        time.Sleep(10 * time.Second)
        process(data)
    }()
    
    // Правильно
    go func(ctx context.Context) {
        select {
        case <-time.After(10 * time.Second):
            process(data)
        case <-ctx.Done():
            return // Выход при отмене
        }
    }(ctx)
    

Оптимизация производительности

// Используйте пул таймеров для частых операций
var timerPool = sync.Pool{
    New: func() interface{} {
        return time.NewTimer(time.Second)
    },
}

func withOptimizedTimeout(ctx context.Context, d time.Duration) (context.Context, func()) {
    timer := timerPool.Get().(*time.Timer)
    timer.Reset(d)
    
    ctx, cancel := context.WithCancel(ctx)
    
    stop := func() {
        cancel()
        if timer.Stop() {
            timerPool.Put(timer)
        }
    }
    
    go func() {
        select {
        case <-timer.C:
            cancel()
        case <-ctx.Done():
            stop()
        }
    }()
    
    return ctx, stop
}

Заключение

context.WithTimeout — мощный инструмент для управления временем выполнения операций в Go. Ключевые принципы:

  • Всегда используйте defer cancel()
  • Проверяйте ctx.Done() в долгих операциях
  • Правильно обрабатывайте context.DeadlineExceeded
  • Каскадируйте контексты для сложных операций
  • Не передавайте отмененные контексты в новые операции

Правильное использование контекстов с таймаутами делает приложения более устойчивыми, предсказуемыми и эффективными в использовании ресурсов.

Как написать context.Timeout? | PrepBro