应避免将数据库查询、模板渲染、邮件发送等耗时操作直接放入 time.ticker 的定时逻辑中,而应仅用 ticker 触发轻量任务(如发信号或投递消息),再由工作协程异步处理报表生成与分发。

用 time.Ticker 做基础定时,但别直接跑报表逻辑
定时报表系统最常见错误,是把数据库查询、模板渲染、邮件发送全塞进 ticker.C 的 goroutine 里。一旦某次报表生成卡住(比如 DB 慢、SMTP 超时),后续所有 tick 都会堆积,最后并发爆炸或 panic。
正确做法是用 time.Ticker 只负责“发信号”,真正干活交给独立 goroutine + 限流控制:
- 每次
ticker.C触发时,只往一个带缓冲的chan struct{}发送一次信号 - 另起一个 goroutine 永久监听该 channel,收到信号就启动报表任务,并用
sync.Once或状态标记避免重复触发 - 关键:加超时控制,比如用
context.WithTimeout(ctx, 5 * time.Minute)包裹整个报表流程
database/sql 查询要配 SetMaxOpenConns 和上下文超时
报表查询常涉及 JOIN、GROUP BY、大表扫描,不设限容易拖垮数据库连接池,或让定时任务无限 hang 在 rows.Next() 上。
Go 的 sql.DB 默认不限制连接数,生产环境必须显式配置:
-
db.SetMaxOpenConns(5)—— 报表类读多写少场景,5~10 足够,避免 DB 端连接耗尽 -
db.SetConnMaxLifetime(30 * time.Minute)—— 防止长连接被中间件(如 RDS Proxy)静默断开 - 所有查询必须传入带超时的
context.Context,例如db.QueryContext(ctx, sql, args...),超时时间建议 ≤ 整个报表流程 timeout 的 70%
用 gomail 发邮件时,SetHeader("To") 不能传空字符串
自动发送报表邮件最隐蔽的坑:收件人列表为空时,gomail 不报错也不发信,而是静默跳过 —— 日志里连 warning 都没有,你只能发现“报表生成了,但没人收到”。
原因在于 gomail.Message 对 SetHeader("To", "") 的处理逻辑是忽略,而非报错。修复方式很简单:
- 发信前检查收件人切片长度:
if len(toList) == 0 { log.Warn("no recipients, skip sending"); return } - 构造
gomail.Message时,To字段必须是非空字符串数组,且每个元素需通过mail.ParseAddress校验格式 - SMTP 连接建议复用
gomail.Dialer实例,避免每次发信都新建 TLS 连接(尤其在高频小报表场景下)
生成 Excel 报表优先选 excelize,别碰 tealeg/xlsx
tealeg/xlsx 已归档多年,不支持 Go modules,且对中文、日期、合并单元格等常见报表需求支持极差;而 excelize 是当前事实标准,但新手容易踩两个坑:
- 写入大量数据时,别用
f.SetCellValue("Sheet1", "A1", v)循环调用 —— 性能极差。改用f.SetSheetRow("Sheet1", "A1", &row)批量写入切片 - 导出前务必调用
f.SaveAs("/tmp/report.xlsx"),而不是f.WriteToBuffer()后直接读取 buffer —— 后者在含图片、公式或条件格式时可能损坏文件 - 如果报表需按天分 Sheet,注意
f.NewSheet()返回的是 int 类型 sheet ID,后续操作要用f.GetSheetName(id)拿名字,不能硬编码 "Sheet2"
复杂报表的真正难点不在定时或发信,而在数据一致性:比如 02:00 定时查昨天数据,但 ETL 任务 02:05 才写完分区,这时查到的就是空结果。得靠外部元数据表或时间戳校验兜底,这个没法靠 Go 库解决。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











