select и таймауты
select: switch для каналов
select позволяет одновременно ждать несколько канальных операций и реагировать на первую готовую:
select {
case v := <-ch1:
fmt.Println("получили из ch1:", v)
case v := <-ch2:
fmt.Println("получили из ch2:", v)
case ch3 <- 99:
fmt.Println("отправили в ch3")
}Правила:
- Если готов один кейс — выполняется он
- Если готовы несколько — выбирается случайный (защита от starvation)
- Если ни один не готов —
selectблокируется до первого готового
Таймауты через time.After
time.After(d) возвращает канал, который получит значение через d. В связке с select — готовый таймаут:
func fetchWithTimeout(url string) (string, error) {
result := make(chan string, 1)
go func() {
// симулируем медленный запрос
time.Sleep(2 * time.Second)
result <- "данные с " + url
}()
select {
case data := <-result:
return data, nil
case <-time.After(1 * time.Second):
return "", fmt.Errorf("таймаут: %s не ответил за 1s", url)
}
}Важно: time.After создаёт таймер, который не отменяется при досрочном выходе из select. В горячих путях лучше time.NewTimer с явным Stop():
timer := time.NewTimer(1 * time.Second)
defer timer.Stop() // предотвращаем утечку таймера
select {
case data := <-result:
return data, nil
case <-timer.C:
return "", errors.New("таймаут")
}default: неблокирующие операции
default в select выполняется если ни один кейс не готов — делает операцию неблокирующей:
// Неблокирующее чтение
select {
case v := <-ch:
fmt.Println("прочитали:", v)
default:
fmt.Println("канал пуст")
}
// Неблокирующая отправка
select {
case ch <- value:
fmt.Println("отправили")
default:
fmt.Println("канал заполнен, пропускаем")
}Паттерн «проверить без блокировки»:
func tryReceive(ch <-chan int) (int, bool) {
select {
case v := <-ch:
return v, true
default:
return 0, false
}
}Done channel: сигнал отмены
Классический паттерн для остановки горутин — канал-сигнал done:
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("получили сигнал остановки")
return
default:
// выполняем работу
fmt.Println("работаем...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(2 * time.Second)
close(done) // сигнализируем всем горутинам
time.Sleep(100 * time.Millisecond)
fmt.Println("завершено")
}chan struct{} — пустой канал, не несёт данных, только сигнал. close(done) разблокирует все горутины, читающие из него — в отличие от отправки одного значения.
context: стандартный способ отмены
Пакет context — современная замена done channel для отмены операций с дедлайнами:
import "context"
func worker(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled или context.DeadlineExceeded
default:
// работа
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
// Отмена по таймауту
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // всегда вызывать! освобождает ресурсы
if err := worker(ctx); err != nil {
fmt.Println("worker остановлен:", err)
}
}context.WithCancel — ручная отмена:
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
go worker(ctx)
go worker(ctx)
time.Sleep(2 * time.Second)
cancel() // останавливает все 3 горутины одновременноcontext передаётся первым аргументом по всей цепочке вызовов — это конвенция Go.
Паттерн: periodic ticker
time.Tick для периодических задач:
func monitor(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
fmt.Println("тик в", t.Format("15:04:05"))
// делаем периодическую работу
case <-ctx.Done():
fmt.Println("монитор остановлен")
return
}
}
}Разница между time.Tick и time.NewTicker:
time.Tick— удобно, но таймер нельзя остановить (утечка в горутинах)time.NewTicker— явныйStop(), всегда используй в горутинах
Паттерн: или-канал (or-channel)
Объединение нескольких done-каналов в один:
func or(channels ...<-chan struct{}) <-chan struct{} {
switch len(channels) {
case 0:
return nil
case 1:
return channels[0]
}
orDone := make(chan struct{})
go func() {
defer close(orDone)
switch len(channels) {
case 2:
select {
case <-channels[0]:
case <-channels[1]:
}
default:
select {
case <-channels[0]:
case <-channels[1]:
case <-channels[2]:
case <-or(append(channels[3:], orDone)...):
}
}
}()
return orDone
}Сигнализирует когда любой из каналов закрыт — полезно для «первый ответивший побеждает».