Avatar ·

Как выполнять повторяющиеся задачи с заданным интервалом в Go?

Всем привет! Я новичок в Go и пишу небольшой сервис для мониторинга. Мне нужно, чтобы каждые 5 секунд программа проверяла статус удаленного сервера и записывала результат в лог. Я попробовал использовать `time.Sleep()` в бесконечном цикле, но это выглядит костыльно и, кажется, неправильно блокирует всю программу. Вот мой текущий код: ```go package main import ( "fmt" "time" ) func main() { for { fmt.Println("Проверяю сервер...") // тут какая-то логика проверки time.Sleep(5 * time.Second) } } ``` Проблема в том, что если я захочу остановить этот цикл (например, по сигналу завершения программы), то придется ждать окончания `Sleep`. Кроме того, я слышал, что в Go есть горутины и каналы, но не понимаю, как их применить для этой задачи. Подскажите, есть ли в Go стандартный способ выполнять повторяющиеся действия через равные промежутки времени? Желательно с возможностью graceful shutdown. Буду благодарен за примеры кода. <<>>

Использование time.NewTicker для периодических задач

Да, в Go есть элегантное решение — пакет time предоставляет тип Ticker. Он создает канал, который отправляет текущее время через заданные интервалы. Это позволяет не блокировать горутину полностью и корректно завершать работу.

Вот как можно переписать ваш пример с использованием тикера:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Создаем тикер с интервалом 5 секунд
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop() // освобождаем ресурсы при завершении

    // Канал для сигналов ОС (Ctrl+C, SIGTERM)
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    fmt.Println("Мониторинг запущен. Нажмите Ctrl+C для остановки.")

    // Бесконечный цикл с select
    for {
        select {
        case <-ticker.C:
            // Сработал тикер — выполняем задачу
            fmt.Println("Проверяю сервер...")
            // Здесь ваша логика проверки
        case sig := <-sigChan:
            // Получен сигнал завершения
            fmt.Printf("Получен сигнал %v. Завершаю работу...\n", sig)
            return
        }
    }
}

Как это работает

  • time.NewTicker(d) создает тикер, который отправляет значение в канал C каждые d.
  • defer ticker.Stop() гарантирует, что тикер будет остановлен при выходе из функции, чтобы не было утечки горутин.
  • select позволяет слушать несколько каналов одновременно. Когда срабатывает тикер — выполняется задача. Когда приходит сигнал — программа завершается.
  • signal.Notify перенаправляет системные сигналы (SIGINT, SIGTERM) в канал, что позволяет корректно завершить программу без ожидания окончания таймера.

Дополнительные возможности

Если вам нужно выполнить задачу сразу при запуске, а затем повторять через интервал, используйте time.NewTicker в комбинации с первоначальным вызовом функции:

func main() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    // Выполняем сразу
    doWork()

    for {
        select {
        case <-ticker.C:
            doWork()
        case <-sigChan:
            return
        }
    }
}

func doWork() {
    fmt.Println("Проверяю сервер...")
}

Также обратите внимание на time.Tick() — это упрощенная версия, но она не позволяет остановить тикер, поэтому для production-кода лучше использовать time.NewTicker.

Ваш исходный подход с time.Sleep работает, но не дает контроля над прерыванием. Тикер решает эту проблему и является идиоматичным способом в Go.

<<>>

Альтернатива: time.Timer и рекурсивный подход

Если вам нужно более гибкое управление (например, динамически менять интервал или пропускать выполнение при ошибках), можно использовать time.Timer в комбинации с рекурсией через горутину:

func startWorker(interval time.Duration, stop chan struct{}) {
    go func() {
        timer := time.NewTimer(interval)
        defer timer.Stop()

        for {
            select {
            case <-timer.C:
                // Выполняем работу
                fmt.Println("Работаю...")

                // Сбрасываем таймер для следующего выполнения
                timer.Reset(interval)
            case <-stop:
                fmt.Println("Остановлен")
                return
            }
        }
    }()
}

func main() {
    stop := make(chan struct{})
    startWorker(5*time.Second, stop)

    // Ждем сигнал
    time.Sleep(20 * time.Second)
    close(stop) // останавливаем воркер
    time.Sleep(1 * time.Second)
}

Этот подход позволяет легко создавать несколько воркеров с разными интервалами и управлять ими через каналы. Однако он чуть сложнее, чем тикер, и требует ручного сброса таймера. Для простых повторяющихся задач лучше использовать time.NewTicker из первого ответа.