← Назад к вопросам
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 операций и т.д.
Подход
- Запустить функцию в отдельной горутине
- Функция отправляет результат в канал
- Использовать
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)
}
}
Ключевые выводы
- select — механизм для ожидания одного из нескольких каналов
- буферизованный канал — избегает утечки горутин
- context.Done() — синал об отмене контекста
- ctx.Err() — тип ошибки (DeadlineExceeded или Canceled)
- defer cancel() — гарантирует очистку ресурсов
Этот паттерн — стандартный в Go для работы с таймаутами и отменой операций в production коде.