[每周一更]-(第144期):Go 定时任务的使用:从基础到进阶
文章目录
- 一、使用 time 包实现定时任务
- 1.1 使用 `time.Ticker`
- 1.2 使用 `time.After`
- 二、使用 cron 表达式调度任务(推荐)
- 2.1 安装
- 2.2 基本用法
- 正确做法:使用 `sync.WaitGroup` 或 `signal.Notify`
- 方法一:使用 `os.Signal` 优雅监听退出信号
- 方法二:主线程保活 + 发起 API 请求等逻辑
- 不推荐仅用 `select {}` 的场景
- 总结
- 2.3 Cron 表达式格式
- 2.4 使用带秒的 Cron 表达式
- 三、任务控制:停止、重启、带上下文
- 四、多个任务调度
- 五、进阶:结合业务任务使用
- 六、总结
- 七、建议实践
- **八、Go 定时任务实战项目(完整版)**,包括:
- 项目结构
- 1. 配置文件支持(config/config.yaml)
- 2. main.go
- 3. tasks/api_task.go
- 4. tasks/report_task.go
- 5. Dockerfile
- 6. systemd 服务支持(cron.service)
- 可选扩展点
- 项目打包
- 资源
常规使用习惯,大概率都是crontab脚本来分离业务逻辑,但是有时候在具体项目代码中,也会使用到定时任务的操作,以下几个我用过的方式来分享一下:
在日常开发中,我们经常会遇到 定时执行任务 的需求,比如:
- 每天凌晨备份数据
- 每隔 10 秒轮询服务状态
- 每周一清理日志文件
虽然 Go 标准库没有专门的任务调度包,但它提供了丰富的时间处理功能,配合第三方库可实现非常强大的定时任务系统。本文将介绍:
- Go 原生定时器的使用
time.Ticker
与time.After
- 基于 cron 表达式的定时任务
- 多任务调度与取消
- 推荐的第三方库:
robfig/cron
一、使用 time 包实现定时任务
Go 标准库中的 time
包提供了简单易用的时间控制函数。
1.1 使用 time.Ticker
每隔固定时间执行一次任务:
package mainimport ("fmt""time"
)func main() {ticker := time.NewTicker(5 * time.Second)defer ticker.Stop()for {select {case t := <-ticker.C:fmt.Println("执行任务时间:", t)}}
}
上面代码会每 5 秒执行一次任务,直到程序退出。
1.2 使用 time.After
只执行一次任务(延迟执行):
func main() {fmt.Println("等待5秒...")time.Sleep(5 * time.Second)fmt.Println("开始执行任务")
}
二、使用 cron 表达式调度任务(推荐)
Go 没有内置 cron 表达式解析器,但 robfig/cron 是最流行的调度库之一,功能强大,语法熟悉。
2.1 安装
go get github.com/robfig/cron/v3
2.2 基本用法
package mainimport ("fmt""github.com/robfig/cron/v3"
)func main() {c := cron.New()// 每分钟执行一次c.AddFunc("* * * * *", func() {fmt.Println("每分钟执行一次任务")})c.Start()// 阻塞主线程(示例中直接 sleep)select {}
}
以上是单独定时任务使用,里边的select{} ,是 Go 中一种阻塞主线程、保持程序运行的常见写法,但它的确会导致 主 goroutine 被永久挂起,从而无法继续执行其他逻辑(比如发起 API 请求、控制台交互等)。
单独使用引起整个项目中其他逻辑挂起,所以一般通过如下替代:
正确做法:使用 sync.WaitGroup
或 signal.Notify
下面是两种推荐方式,既能阻塞主线程防止退出,又能支持优雅退出、处理请求等行为:
方法一:使用 os.Signal
优雅监听退出信号
package mainimport ("fmt""os""os/signal""syscall""github.com/robfig/cron/v3"
)func main() {c := cron.New()c.AddFunc("*/10 * * * * *", func() {fmt.Println("每10秒执行一次任务")})c.Start()// 监听退出信号(支持后续请求或中止)quit := make(chan os.Signal, 1)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)fmt.Println("定时任务已启动,按 Ctrl+C 退出...")<-quitfmt.Println("收到退出信号,停止定时任务")c.Stop()
}
适合实际应用,能处理任务 + 后续请求 + 优雅退出。
方法二:主线程保活 + 发起 API 请求等逻辑
如果你有主逻辑,比如 Web 服务或某个任务,可以这样写:
func main() {// 启动定时任务startCronTask()// 启动你的主服务,如监听 HTTP 或进行其他处理startHTTPServer()// 保持主进程运行(或处理退出信号)select {} // 或用 signal.Notify 替代
}
不推荐仅用 select {}
的场景
使用 select {}
虽然可以快速阻塞主线程,但无法:
- 退出程序
- 接受信号
- 执行并发任务控制
总结
场景 | 建议写法 |
---|---|
快速测试任务 | 可用 select {} 临时阻塞 |
实际服务、API任务 | 用 signal.Notify 监听退出 |
主线程还有其他任务(如 Web) | 多 goroutine + channel 控制 |
2.3 Cron 表达式格式
标准 5 字段格式(秒可选):
* * * * * → 分 时 日 月 星期
示例:
表达式 | 含义 |
---|---|
0 0 * * * | 每天 0 点执行一次 |
*/10 * * * * | 每 10 分钟执行一次 |
30 9 * * 1 | 每周一 9:30 执行 |
2.4 使用带秒的 Cron 表达式
c := cron.New(cron.WithSeconds())
c.AddFunc("*/5 * * * * *", func() {fmt.Println("每5秒执行一次任务")
})
三、任务控制:停止、重启、带上下文
你可以通过 cron.EntryID
来管理任务,例如取消某个任务:
id, _ := c.AddFunc("*/1 * * * *", func() {fmt.Println("正在执行任务")
})c.Remove(id) // 移除定时任务
也可以使用带 context.Context
的任务,实现超时控制。
四、多个任务调度
你可以添加多个不同任务:
c := cron.New()c.AddFunc("0 * * * *", func() {fmt.Println("每小时整点执行")
})
c.AddFunc("30 9 * * *", func() {fmt.Println("每天早上9:30执行")
})
c.Start()
五、进阶:结合业务任务使用
假设你要实现:每天0点生成报告,可以这样封装:
func generateReport() {// 业务逻辑fmt.Println("生成日报成功")
}func scheduleReportTask() {c := cron.New()c.AddFunc("0 0 * * *", generateReport)c.Start()
}
六、总结
技术方式 | 特点 |
---|---|
time.Ticker | 简单、适合循环任务 |
time.After | 单次延迟执行 |
robfig/cron | 强大、支持 Cron 表达式、任务管理 |
七、建议实践
- 简单轮询:用
time.Ticker
- 周期任务:用
robfig/cron
- 支持取消/错误处理:结合
context
和log
八、Go 定时任务实战项目(完整版),包括:
- API请求 + 日报任务
- 支持日志输出和错误处理
- 使用配置文件控制调度逻辑
- 可部署为服务(带 Dockerfile 和 systemd 示例)
- 支持日志平台或数据库扩展
- 使用
context.WithTimeout
控制请求
项目结构
go-cron-service/
├── config/
│ └── config.yaml
├── logs/
│ └── ...
├── tasks/
│ ├── api_task.go
│ └── report_task.go
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
└── cron.service # systemd服务配置
1. 配置文件支持(config/config.yaml)
api_url: "https://httpbin.org/get"
request_timeout: 5sschedules:api_task: "*/10 * * * * *"report_task: "0 0 0 * * *"
2. main.go
package mainimport ("fmt""os""os/signal""syscall""time""go-cron-service/tasks""gopkg.in/yaml.v3""github.com/robfig/cron/v3""io/ioutil"
)type Config struct {APIURL string `yaml:"api_url"`RequestTimeout time.Duration `yaml:"request_timeout"`Schedules map[string]string `yaml:"schedules"`
}var AppConfig Configfunc loadConfig() error {data, err := ioutil.ReadFile("config/config.yaml")if err != nil {return err}return yaml.Unmarshal(data, &AppConfig)
}func main() {if err := loadConfig(); err != nil {panic("配置加载失败: " + err.Error())}c := cron.New(cron.WithSeconds())c.AddFunc(AppConfig.Schedules["api_task"], func() {tasks.FetchAPI(AppConfig.APIURL, AppConfig.RequestTimeout)})c.AddFunc(AppConfig.Schedules["report_task"], tasks.GenerateReport)c.Start()fmt.Println("Cron 服务已启动,按 Ctrl+C 退出")quit := make(chan os.Signal, 1)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitfmt.Println("接收到中断信号,退出...")c.Stop()
}
3. tasks/api_task.go
package tasksimport ("context""fmt""io""net/http""time"
)func FetchAPI(apiURL string, timeout time.Duration) {ctx, cancel := context.WithTimeout(context.Background(), timeout)defer cancel()req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)if err != nil {fmt.Println("构建请求失败:", err)return}resp, err := http.DefaultClient.Do(req)if err != nil {fmt.Println("请求失败:", err)return}defer resp.Body.Close()body, _ := io.ReadAll(resp.Body)fmt.Printf("[%s] 请求成功,长度: %d\n", time.Now().Format("15:04:05"), len(body))
}
4. tasks/report_task.go
package tasksimport ("fmt""os""time"
)func GenerateReport() {now := time.Now().Format("2006-01-02")filePath := fmt.Sprintf("logs/report-%s.txt", now)_ = os.MkdirAll("logs", 0755)file, err := os.Create(filePath)if err != nil {fmt.Println("创建报告失败:", err)return}defer file.Close()content := fmt.Sprintf("日报生成时间:%s\n状态:正常\n", time.Now().Format("2006-01-02 15:04:05"))file.WriteString(content)fmt.Println("报告已生成:", filePath)
}
5. Dockerfile
FROM golang:1.22-alpineWORKDIR /app
COPY . .RUN go build -o cron-service main.goCMD ["./cron-service"]
构建并运行:
docker build -t go-cron-service .
docker run -v $(pwd)/logs:/app/logs go-cron-service
6. systemd 服务支持(cron.service)
[Unit]
Description=Go Cron 定时任务服务
After=network.target[Service]
Type=simple
ExecStart=/usr/local/bin/cron-service
WorkingDirectory=/opt/go-cron-service
Restart=always
StandardOutput=journal
StandardError=journal[Install]
WantedBy=multi-user.target
部署方法:
sudo cp cron.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable cron.service
sudo systemctl start cron.service
可选扩展点
功能 | 说明 |
---|---|
日志接入 ELK、Loki | 使用 logrus/zap 结构化日志 |
写入数据库 | 可记录任务结果到 MySQL/Postgres |
热加载任务 | 使用 fsnotify 监听配置变更 |
Web 管理界面 | 提供前端管理和动态增删任务(Gin + cron) |
项目打包
你可以将此项目上传至 GitHub:
gh repo create go-cron-service --public --source=.
git add .
git commit -m "init: go-cron-service"
git push -u origin main
资源
- Go time 包官方文档
- robfig/cron GitHub 项目