Ответ
Основная проблема — это небезопасное управление жизненным циклом тикера и возможная утечка ресурсов (resource leak).
Вызов ticker.Stop()
из другой горутины является плохой практикой и анти-паттерном. Хотя это не всегда приводит к панике, это создает состояние гонки и делает код хрупким и непредсказуемым.
Почему это плохо?
- Утечка ресурсов: Если горутина с
select
завершится раньше, чем будет вызванticker.Stop()
, то внутренний механизм тикера (включая его собственную горутину) никогда не будет остановлен и останется в памяти до конца работы программы. - Сложность синхронизации: Правильно скоординировать момент остановки тикера из другой горутины сложно. Можно случайно остановить тикер, который все еще нужен, или не остановить тот, который уже не используется.
Важно: Никогда не пытайтесь закрывать канал тикера вручную (
close(ticker.C)
). Это вызовет панику, так как тикер может попытаться отправить в него значение. Толькоticker.Stop()
должен управлять его жизненным циклом.
Правильный (идиоматичный) подход
Идиоматичный и безопасный способ — управлять тикером в той же горутине, где он используется. Для graceful shutdown (изящного завершения) используется дополнительный канал (done
или quit
) или context
.
Горутина с select
слушает одновременно и канал тикера, и канал завершения. Остановка тикера (ticker.Stop()
) происходит после выхода из цикла select
.
Пример с использованием context
(предпочтительный способ):
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
// Создаем тикер внутри горутины, которая его использует
ticker := time.NewTicker(1 * time.Second)
// Гарантируем, что тикер будет остановлен при выходе из функции
defer ticker.Stop()
fmt.Println("Worker started. Ticker is running.")
for {
select {
case <-ctx.Done(): // Если контекст отменен (сигнал к завершению)
fmt.Println("Worker stopping. Cleaning up ticker.")
return // Выходим из цикла и функции, defer ticker.Stop() сработает
case t := <-ticker.C: // Получаем событие от тикера
fmt.Println("Tick at", t.Format("15:04:05"))
}
}
}
func main() {
// Создаем контекст с таймаутом на 5 секунд
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Важно вызывать cancel, чтобы освободить ресурсы контекста
worker(ctx)
fmt.Println("Main function finished.")
}
В этом примере worker
полностью контролирует жизненный цикл тикера. Внешний код (в main
) лишь подает сигнал к завершению через context
. Это делает код безопасным, предсказуемым и свободным от утечек ресурсов.