
文章目录
-
- [一、使用 time 包实现定时任务](#一、使用 time 包实现定时任务)
-
- [1.1 使用 `time.Ticker`](#1.1 使用
time.Ticker
) - [1.2 使用 `time.After`](#1.2 使用
time.After
)
- [1.1 使用 `time.Ticker`](#1.1 使用
- [二、使用 cron 表达式调度任务(推荐)](#二、使用 cron 表达式调度任务(推荐))
-
- [2.1 安装](#2.1 安装)
- [2.2 基本用法](#2.2 基本用法)
- [正确做法:使用 `sync.WaitGroup` 或 `signal.Notify`](#正确做法:使用
sync.WaitGroup
或signal.Notify
) -
- [方法一:使用 `os.Signal` 优雅监听退出信号](#方法一:使用
os.Signal
优雅监听退出信号) - [方法二:主线程保活 + 发起 API 请求等逻辑](#方法二:主线程保活 + 发起 API 请求等逻辑)
- [方法一:使用 `os.Signal` 优雅监听退出信号](#方法一:使用
- [不推荐仅用 `select {}` 的场景](#不推荐仅用
select {}
的场景) - 总结
-
- [2.3 Cron 表达式格式](#2.3 Cron 表达式格式)
- [2.4 使用带秒的 Cron 表达式](#2.4 使用带秒的 Cron 表达式)
- 三、任务控制:停止、重启、带上下文
- 四、多个任务调度
- 五、进阶:结合业务任务使用
- 六、总结
- 七、建议实践
- [**八、Go 定时任务实战项目(完整版)**,包括:](#八、Go 定时任务实战项目(完整版),包括:)
- [1. 配置文件支持(config/config.yaml)](#1. 配置文件支持(config/config.yaml))
- [2. main.go](#2. main.go)
- [3. tasks/api_task.go](#3. tasks/api_task.go)
- [4. tasks/report_task.go](#4. tasks/report_task.go)
- [5. Dockerfile](#5. Dockerfile)
- [6. systemd 服务支持(cron.service)](#6. systemd 服务支持(cron.service))
- 资源
常规使用习惯,大概率都是crontab脚本来分离业务逻辑,但是有时候在具体项目代码中,也会使用到定时任务的操作,以下几个我用过的方式来分享一下:
在日常开发中,我们经常会遇到 定时执行任务 的需求,比如:
- 每天凌晨备份数据
- 每隔 10 秒轮询服务状态
- 每周一清理日志文件
虽然 Go 标准库没有专门的任务调度包,但它提供了丰富的时间处理功能,配合第三方库可实现非常强大的定时任务系统。本文将介绍:
- Go 原生定时器的使用
time.Ticker
与time.After
- 基于 cron 表达式的定时任务
- 多任务调度与取消
- 推荐的第三方库:
robfig/cron
一、使用 time 包实现定时任务
Go 标准库中的 time
包提供了简单易用的时间控制函数。
1.1 使用 time.Ticker
每隔固定时间执行一次任务:
go
package main
import (
"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
只执行一次任务(延迟执行):
go
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 基本用法
go
package main
import (
"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
优雅监听退出信号
go
package main
import (
"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 退出...")
<-quit
fmt.Println("收到退出信号,停止定时任务")
c.Stop()
}
适合实际应用,能处理任务 + 后续请求 + 优雅退出。
方法二:主线程保活 + 发起 API 请求等逻辑
如果你有主逻辑,比如 Web 服务或某个任务,可以这样写:
go
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 表达式
go
c := cron.New(cron.WithSeconds())
c.AddFunc("*/5 * * * * *", func() {
fmt.Println("每5秒执行一次任务")
})
三、任务控制:停止、重启、带上下文
你可以通过 cron.EntryID
来管理任务,例如取消某个任务:
go
id, _ := c.AddFunc("*/1 * * * *", func() {
fmt.Println("正在执行任务")
})
c.Remove(id) // 移除定时任务
也可以使用带 context.Context
的任务,实现超时控制。
四、多个任务调度
你可以添加多个不同任务:
go
c := cron.New()
c.AddFunc("0 * * * *", func() {
fmt.Println("每小时整点执行")
})
c.AddFunc("30 9 * * *", func() {
fmt.Println("每天早上9:30执行")
})
c.Start()
五、进阶:结合业务任务使用
假设你要实现:每天0点生成报告,可以这样封装:
go
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
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)
yaml
api_url: "https://httpbin.org/get"
request_timeout: 5s
schedules:
api_task: "*/10 * * * * *"
report_task: "0 0 0 * * *"
2. main.go
go
package main
import (
"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 Config
func 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)
<-quit
fmt.Println("接收到中断信号,退出...")
c.Stop()
}
3. tasks/api_task.go
go
package tasks
import (
"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
go
package tasks
import (
"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
dockerfile
FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o cron-service main.go
CMD ["./cron-service"]
构建并运行:
bash
docker build -t go-cron-service .
docker run -v $(pwd)/logs:/app/logs go-cron-service
6. systemd 服务支持(cron.service)
ini
[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
部署方法:
bash
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 项目