You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Golang中实现Web服务后台异步定时触发并重启的功能?

需求与问题

需要在服务端实现以下功能:

  • 后台运行,等待指定时长后触发事件
  • 事件执行完成后自动重启定时器,循环往复
  • 功能需异步执行:定时器在后台启动,到期后调用执行函数,函数执行完毕后异步重启定时器,不阻塞主代码
  • 每次定时时长需动态计算

当前实现方案中,startTimer函数内的循环会阻塞调用它的goroutine(因<-jt.t.C是阻塞操作),无法满足异步不阻塞的要求,寻求改进方式。

现有代码示例

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "os"
    "strconv"
    "strings"
    "time"
)

const INTERVAL_PERIOD time.Duration = 24 * time.Hour

var HOUR_TO_TICK int
var MINUTE_TO_TICK int
var SECOND_TO_TICK int

type jobTicker struct {
    t *time.Timer
}

func setConfig(appconfs map[string]string) bool {
    timeOfEventTrigger := appconfs["Task_Trigger_Time"]
    clockTime := strings.Split(timeOfEventTrigger, ":")

    fmt.Println("触发事件的固定时间点:", clockTime)

    HOUR_TO_TICK, _ = strconv.Atoi(clockTime[0])
    MINUTE_TO_TICK, _ = strconv.Atoi(clockTime[1])
    SECOND_TO_TICK, _ = strconv.Atoi(clockTime[2])

    return true
}

func readConfigFromJson(configPath string) bool {
    fmt.Printf("从%s加载配置\n", configPath)
    configMap := map[string]string{}
    configfile, err := os.Open(configPath)
    if err != nil {
        fmt.Printf("加载配置失败:%v\n", err)
        return false
    }
    byteValue, err := ioutil.ReadAll(configfile)
    if err != nil {
        fmt.Printf("读取配置文件失败:%v\n", err)
        return false
    }
    err = json.Unmarshal(byteValue, &configMap)
    if err != nil {
        fmt.Printf("JSON解析失败:%v\n", err)
        return false
    }

    defer configfile.Close()
    return setConfig(configMap)
}

func getNextTickDuration() time.Duration {
    now := time.Now()
    nextTick := time.Date(now.Year(), now.Month(), now.Day(), HOUR_TO_TICK, MINUTE_TO_TICK, SECOND_TO_TICK, 0, time.Local)

    if nextTick.Before(now) {
        nextTick = nextTick.Add(INTERVAL_PERIOD)
    }
    fmt.Println("当前时间:", now)
    fmt.Println("下次触发时间:", nextTick)
    waitDuration := nextTick.Sub(time.Now())
    fmt.Println("等待时长:", waitDuration)
    return waitDuration
}

func NewJobTicker() jobTicker {
    fmt.Println("生成新的任务定时器")
    return jobTicker{time.NewTimer(getNextTickDuration())}
}

func (jt jobTicker) updateJobTicker() {
    fmt.Println("更新任务定时器")
    jt.t.Reset(getNextTickDuration())
}

func startTimer() {
    jt := NewJobTicker()

    for {
        <-jt.t.C
        fmt.Println(time.Now(), "- 触发事件")
        // 执行业务逻辑
        jt.updateJobTicker()
    }
}

配置文件示例

// config.json
{
    "Task_Trigger_Time": "12:30:00"
}

改进方案

核心问题是原startTimer函数的循环会阻塞调用方,只需将定时器循环逻辑放入独立goroutine中,即可实现异步不阻塞;同时优化结构体方法的接收者类型,避免值传递可能带来的混淆。

改进后的关键代码

  1. 修改jobTicker的方法为指针接收者:
type jobTicker struct {
    t *time.Timer
}

func NewJobTicker() *jobTicker {
    fmt.Println("生成新的任务定时器")
    return &jobTicker{time.NewTimer(getNextTickDuration())}
}

func (jt *jobTicker) updateJobTicker() {
    fmt.Println("更新任务定时器")
    jt.t.Reset(getNextTickDuration())
}
  1. 异步启动定时器循环:
func startTimer() {
    // 启动独立goroutine处理定时器逻辑,不阻塞调用方
    go func() {
        jt := NewJobTicker()
        for {
            <-jt.t.C
            fmt.Println(time.Now(), "- 触发事件")
            
            // 若业务逻辑耗时较长,可将其放入独立goroutine执行
            // 确保业务逻辑完成后再重启定时器,符合需求
            func() {
                // 这里执行你的业务逻辑
                // do your logic here

                // 业务逻辑完成后重启定时器
                jt.updateJobTicker()
            }()
        }
    }()
}

说明

  • 调用startTimer()时,会立即返回,主程序不受阻塞,定时器逻辑在后台goroutine中运行
  • 业务逻辑若耗时,可封装在内部goroutine中,避免阻塞定时器的下次触发判断(若需等待业务逻辑完成再重启定时器,则保持当前顺序即可)
  • 使用指针接收者操作jobTicker,确保对定时器的修改作用于原实例,避免值传递导致的无效操作

内容的提问来源于stack exchange,提问作者Ravi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 19:15:45