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

Wrapper для медленной функции с таймаутом

2.3 Middle🔥 121 комментариев
#Основы Go

Условие

Реализуйте функцию-обертку, которая выполняет медленную операцию с поддержкой таймаута через context.

Сигнатура

func withTimeout(ctx context.Context, fn func() (string, error)) (string, error)

Требования

  • Выполнить функцию fn в горутине
  • Если функция завершилась до таймаута - вернуть её результат
  • Если контекст отменён или истёк таймаут - вернуть ошибку context.Err()
  • Использовать select для ожидания

Пример использования

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

result, err := withTimeout(ctx, func() (string, error) {
    time.Sleep(1 * time.Second)
    return "done", nil
})

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

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

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

Решение

Wrapper с таймаутом — это паттерн для защиты долгоживущих операций от зависаний. Используется повсеместно в production коде для HTTP запросов, database операций и т.д.

Подход

  1. Запустить функцию в отдельной горутине
  2. Функция отправляет результат в канал
  3. Использовать select для ожидания:
    • Результат функции готов → вернуть результат
    • Контекст отменён/таймаут истёк → вернуть ошибку

Реализация

package main

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

func withTimeout(ctx context.Context, fn func() (string, error)) (string, error) {
    // Канал для результата функции
    resultCh := make(chan struct {
        result string
        err    error
    }, 1)  // буферизованный, чтобы горутина не зависла
    
    // Запускаем функцию в горутине
    go func() {
        result, err := fn()
        resultCh <- struct {
            result string
            err    error
        }{result, err}
    }()
    
    // Ждём результат или таймаут
    select {
    case res := <-resultCh:
        return res.result, res.err
    case <-ctx.Done():  // контекст отменён или таймаут
        return "", ctx.Err()
    }
}

func main() {
    // Пример 1: функция завершится до таймаута
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    result, err := withTimeout(ctx, func() (string, error) {
        time.Sleep(1 * time.Second)
        return "done", nil
    })
    
    if err != nil {
        fmt.Printf("Ошибка: %v\n", err)
    } else {
        fmt.Printf("Результат: %s\n", result)  // done
    }
}

Пошаговый анализ

Сценарий 1: Функция завершилась вовремя

1. withTimeout создаёт resultCh
2. Запускает fn в горутине
3. select ждёт:
   - resultCh ← результат (200ms) ✓
   - ctx.Done() ← (2000ms)
4. resultCh готов первым
5. Вернуть результат

Сценарий 2: Таймаут истёк

1. withTimeout создаёт resultCh
2. Запускает fn в горутине (длится 5 секунд)
3. select ждёт:
   - resultCh ← (5000ms)
   - ctx.Done() ← (2000ms) ✓
4. ctx.Done() готов первым
5. Вернуть context.DeadlineExceeded

Полный пример с тестами

package main

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

func withTimeout(ctx context.Context, fn func() (string, error)) (string, error) {
    resultCh := make(chan struct {
        result string
        err    error
    }, 1)
    
    go func() {
        result, err := fn()
        resultCh <- struct {
            result string
            err    error
        }{result, err}
    }()
    
    select {
    case res := <-resultCh:
        return res.result, res.err
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // Пример 1: успешное завершение
    fmt.Println("=== Пример 1: успешно ===")
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    result, err := withTimeout(ctx, func() (string, error) {
        time.Sleep(500 * time.Millisecond)
        return "Hello, World!", nil
    })
    
    fmt.Printf("Результат: %s, Ошибка: %v\n", result, err)
    // Результат: Hello, World!, Ошибка: <nil>
    
    // Пример 2: таймаут
    fmt.Println("\n=== Пример 2: таймаут ===")
    ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    
    result, err = withTimeout(ctx, func() (string, error) {
        time.Sleep(3 * time.Second)
        return "never", nil
    })
    
    fmt.Printf("Результат: %s, Ошибка: %v\n", result, err)
    // Результат: , Ошибка: context deadline exceeded
    
    // Пример 3: ошибка в функции
    fmt.Println("\n=== Пример 3: ошибка в функции ===")
    ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    result, err = withTimeout(ctx, func() (string, error) {
        return "", errors.New("что-то пошло не так")
    })
    
    fmt.Printf("Результат: %s, Ошибка: %v\n", result, err)
    // Результат: , Ошибка: что-то пошло не так
    
    // Пример 4: отмена контекста
    fmt.Println("\n=== Пример 4: отмена контекста ===")
    ctx, cancel = context.WithCancel(context.Background())
    
    go func() {
        time.Sleep(500 * time.Millisecond)
        cancel()  // отменяем контекст
    }()
    
    result, err = withTimeout(ctx, func() (string, error) {
        time.Sleep(2 * time.Second)
        return "never", nil
    })
    
    fmt.Printf("Результат: %s, Ошибка: %v\n", result, err)
    // Результат: , Ошибка: context canceled
}

Вариант с использованием reflect.Select (advanced)

import "reflect"

func withTimeoutReflect(ctx context.Context, fn func() (string, error)) (string, error) {
    resultCh := make(chan struct {
        result string
        err    error
    }, 1)
    
    go func() {
        result, err := fn()
        resultCh <- struct {
            result string
            err    error
        }{result, err}
    }()
    
    cases := []reflect.SelectCase{
        {
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(resultCh),
        },
        {
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(ctx.Done()),
        },
    }
    
    chosen, value, ok := reflect.Select(cases)
    
    if chosen == 0 {  // resultCh готов
        res := value.Interface().(struct {
            result string
            err    error
        })
        return res.result, res.err
    } else {  // ctx.Done() готов
        return "", ctx.Err()
    }
}

Почему буферизованный канал?

// ✅ Буферизованный канал (размер 1)
resultCh := make(chan struct{...}, 1)

// Если таймаут и горутина отправляет результат:
// resultCh <- res  // ✓ успешно, результат сохранён в буфере
// горутина не зависит

// ❌ Безбуферный канал
resultCh := make(chan struct{...})

// Если таймаут и горутина отправляет результат:
// resultCh <- res  // горутина блокируется (нет получателя)
// утечка горутины!

Обработка ошибок контекста

result, err := withTimeout(ctx, fn)

if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        fmt.Println("Таймаут истёк")
    } else if errors.Is(err, context.Canceled) {
        fmt.Println("Контекст отменён")
    } else {
        fmt.Printf("Другая ошибка: %v\n", err)
    }
}

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

func fetchWithTimeout(ctx context.Context, url string) (string, error) {
    return withTimeout(ctx, func() (string, error) {
        resp, err := http.Get(url)
        if err != nil {
            return "", err
        }
        defer resp.Body.Close()
        
        body, err := io.ReadAll(resp.Body)
        return string(body), err
    })
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    result, err := fetchWithTimeout(ctx, "https://api.example.com/data")
    if err != nil {
        fmt.Printf("Ошибка: %v\n", err)
    } else {
        fmt.Printf("Результат: %s\n", result)
    }
}

Ключевые выводы

  1. select — механизм для ожидания одного из нескольких каналов
  2. буферизованный канал — избегает утечки горутин
  3. context.Done() — синал об отмене контекста
  4. ctx.Err() — тип ошибки (DeadlineExceeded или Canceled)
  5. defer cancel() — гарантирует очистку ресурсов

Этот паттерн — стандартный в Go для работы с таймаутами и отменой операций в production коде.