Ответ
Основная проблема — это небезопасное управление жизненным циклом тикера и возможная утечка ресурсов (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. Это делает код безопасным, предсказуемым и свободным от утечек ресурсов.