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

文章目录

    • [一、使用 time 包实现定时任务](#一、使用 time 包实现定时任务)
      • [1.1 使用 `time.Ticker`](#1.1 使用 time.Ticker)
      • [1.2 使用 `time.After`](#1.2 使用 time.After)
    • [二、使用 cron 表达式调度任务(推荐)](#二、使用 cron 表达式调度任务(推荐))
      • [2.1 安装](#2.1 安装)
      • [2.2 基本用法](#2.2 基本用法)
    • [正确做法:使用 `sync.WaitGroup` 或 `signal.Notify`](#正确做法:使用 sync.WaitGroupsignal.Notify)
      • [方法一:使用 `os.Signal` 优雅监听退出信号](#方法一:使用 os.Signal 优雅监听退出信号)
      • [方法二:主线程保活 + 发起 API 请求等逻辑](#方法二:主线程保活 + 发起 API 请求等逻辑)
    • [不推荐仅用 `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 标准库没有专门的任务调度包,但它提供了丰富的时间处理功能,配合第三方库可实现非常强大的定时任务系统。本文将介绍:

  1. Go 原生定时器的使用
  2. time.Tickertime.After
  3. 基于 cron 表达式的定时任务
  4. 多任务调度与取消
  5. 推荐的第三方库: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.WaitGroupsignal.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
  • 支持取消/错误处理:结合 contextlog

八、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

资源

相关推荐
Hello eveybody41 分钟前
C++介绍整数二分与实数二分
开发语言·数据结构·c++·算法
jmlinux2 小时前
从 C 语言计算器到串口屏应用
c语言·开发语言
Mallow Flowers2 小时前
Python训练营-Day31-文件的拆分和使用
开发语言·人工智能·python·算法·机器学习
G探险者3 小时前
为什么 Zookeeper 越扩越慢,而 Nacos 却越扩越快?
分布式·后端
云边小网安3 小时前
java集合篇(六) ---- ListIterator 接口
java·开发语言·青少年编程·java集合
不太厉害的程序员3 小时前
NC65配置xml找不到Bean
xml·java·后端·eclipse
不被定义的程序猿3 小时前
Golang 在 Linux 平台上的并发控制
开发语言·后端·golang
AntBlack3 小时前
Python : AI 太牛了 ,撸了两个 Markdown 阅读器 ,谈谈使用感受
前端·人工智能·后端
陈旭金-小金子4 小时前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
Mikhail_G4 小时前
Python应用八股文
大数据·运维·开发语言·python·数据分析