[每周一更]-(第144期):Go 定时任务的使用:从基础到进阶

在这里插入图片描述


常规使用习惯,大概率都是crontab脚本来分离业务逻辑,但是有时候在具体项目代码中,也会使用到定时任务的操作,以下几个我用过的方式来分享一下:

在日常开发中,我们经常会遇到 定时执行任务 的需求,比如:

  • 每天凌晨备份数据
  • 每隔 10 秒轮询服务状态
  • 每周一清理日志文件

虽然 Go 标准库没有专门的任务调度包,但它提供了丰富的时间处理功能,配合第三方库可实现非常强大的定时任务系统。本文将介绍:

  1. Go 原生定时器的使用
  2. time.Tickertime.After
  3. 基于 cron 表达式的定时任务
  4. 多任务调度与取消
  5. 推荐的第三方库:robfig/cron

一、使用 time 包实现定时任务

Go 标准库中的 time 包提供了简单易用的时间控制函数。

1.1 使用 time.Ticker

每隔固定时间执行一次任务:

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

只执行一次任务(延迟执行):

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 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.WaitGroupsignal.Notify

下面是两种推荐方式,既能阻塞主线程防止退出,又能支持优雅退出、处理请求等行为


方法一:使用 os.Signal 优雅监听退出信号

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 服务或某个任务,可以这样写:

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
  • 支持取消/错误处理:结合 contextlog

八、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: 5s

schedules:
  api_task: "*/10 * * * * *"
  report_task: "0 0 0 * * *"

2. main.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

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

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

FROM golang:1.22-alpine

WORKDIR /app
COPY . .

RUN go build -o cron-service main.go

CMD ["./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

资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ifanatic

觉得对您有用,可以友情打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值