不能直接用 log.Printf 写文件到磁盘,因其每条日志触发一次系统调用和磁盘刷写,高并发下迅速打满 I/O 队列,导致协程阻塞、响应变慢甚至超时;即使 QPS 仅 200,IO wait 也可能占 CPU 时间 15% 以上。

为什么不能直接用 log.Printf 写文件到磁盘
直接调用 log.Printf 配合 os.OpenFile 写日志,每条日志都触发一次系统调用和磁盘刷写,高并发下会迅速打满 I/O 队列,write 系统调用阻塞协程,导致整个服务响应变慢甚至超时。这不是日志量大才出问题——哪怕 QPS 只有 200,若每请求写 3 条日志,IO wait 就可能占到 CPU 时间的 15% 以上。
关键不是“要不要异步”,而是“异步怎么不丢日志、不拖垮内存、不引入新竞争”。单例协程池是折中解:复用 goroutine + channel 缓冲 + 批量刷盘。
sync.Once 初始化单例池时最容易漏掉的三个点
很多人用 sync.Once 包一层初始化,但忽略实际运行时依赖项。常见错误包括:
- 没检查
os.OpenFile返回的*os.File是否为nil,导致后续Writepanic - 把缓冲 channel 容量设成固定值(比如
make(chan *LogEntry, 1000)),但没配对应的 flush 触发阈值,结果 buffer 满了就卡住 sender - 忘记设置
SetOutput或重定向标准 log 的输出目标,导致日志仍走默认 stderr,单例池形同虚设
正确做法是把 file handler、channel、flush ticker 全部在 Once.Do 里原子创建,并做基础健康检查(例如 file.Stat() 确认可写)。
立即学习“go语言免费学习笔记(深入)”;
批量写入时 bufio.Writer 的 Flush 调用时机很关键
用 bufio.NewWriterSize(file, 4096) 能减少系统调用次数,但必须控制 Flush 频率:太勤(比如每次写都 flush)等于没缓存;太懒(比如只在 shutdown 时 flush)会丢日志。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
推荐策略是三条件触发 flush:
- buffer 已满(
w.Available() == 0) - 累积日志条数 ≥ 100
- 距离上次 flush 超过 1s(用
time.AfterFunc或 ticker 控制)
注意:bufio.Writer 的 WriteString 不保证原子性,多 goroutine 并发写同一个 writer 会乱序,必须加锁或确保只有 pool goroutine 调用它。
协程池 shutdown 阶段丢失日志的典型场景
服务收到 SIGTERM 后,主 goroutine 调用 pool.Close(),但若只 close channel 就退出,未消费完的 chan *LogEntry 里的日志就丢了。
安全关闭必须分两步:
- 先 close input channel,通知 worker 停止接收新日志
- 再循环 drain channel 直到空,然后 flush buffer、close file
别依赖 defer 在 main 函数末尾做 cleanup —— 若程序 panic 或被 kill -9,defer 不执行。shutdown 逻辑应显式绑定到信号监听里,且带 timeout(比如最多等 3s,超时强制丢弃剩余日志)。
真正麻烦的是 context cancel 和 panic 恢复之间的交叠:worker goroutine 正在 flush 时被 cancel,file.Write 可能返回 os.ErrClosed,得捕获并跳过,而不是 panic。

















