Как в одной горутине одновременно ожидать события от тикера и другого канала?

Ответ

Это классическая задача для select в цикле for. Горутина должна выполнять периодические действия (по тикеру) и одновременно реагировать на входящие сообщения или сигналы завершения.

Неправильный подход — использовать select с default и time.Sleep. Это создает неэффективный цикл (busy-wait), который постоянно потребляет CPU, просыпаясь и засыпая.

Правильное решение — использовать блокирующий select для ожидания нескольких каналов. Он будет "спать" и не потреблять ресурсы до тех пор, пока в одном из каналов не появится событие.

Вот канонический пример реализации рабочего цикла (worker):

package main

import (
    "fmt"
    "time"
)

func worker(msgChan <-chan string, doneChan <-chan struct{}) {
    // Создаем тикер, который будет срабатывать каждые 2 секунды
    ticker := time.NewTicker(2 * time.Second)
    // Важно остановить тикер, чтобы избежать утечки ресурсов
    defer ticker.Stop()

    for {
        select {
        // Случай 1: Пришло сообщение для обработки
        case msg := <-msgChan:
            fmt.Printf("Получено сообщение: %sn", msg)

        // Случай 2: Сработал тикер для периодической задачи
        case t := <-ticker.C:
            fmt.Printf("Тик в %vn", t.Format(time.Kitchen))

        // Случай 3: Пришел сигнал о завершении работы
        case <-doneChan:
            fmt.Println("Воркер завершает работу...")
            return
        }
    }
}

func main() {
    msgChan := make(chan string, 1)
    doneChan := make(chan struct{})

    go worker(msgChan, doneChan)

    // Отправляем сообщения для обработки
    msgChan <- "Первое задание"
    time.Sleep(3 * time.Second)
    msgChan <- "Второе задание"

    // Ждем 5 секунд и завершаем воркер
    time.Sleep(5 * time.Second)
    close(doneChan)

    // Даем время на graceful shutdown
    time.Sleep(1 * time.Second)
    fmt.Println("Программа завершена")
}

Ключевые моменты этого паттерна:

  1. for-select: Бесконечный цикл с select внутри — основа для долгоживущих горутин.
  2. Блокировка: select блокирует горутину до тех пор, пока один из case не станет доступен для выполнения (чтения или записи).
  3. Graceful Shutdown: Наличие канала doneChan позволяет извне безопасно остановить горутину, давая ей возможность завершить текущие операции и освободить ресурсы.
  4. defer ticker.Stop(): Тикеры — это ресурсы, которые нужно освобождать, иначе они будут работать до конца жизни программы, даже если на них никто не слушает.