用带缓冲channel(如make(chan string, 1024))解耦日志写入,主流程select+default非阻塞发送,后台goroutine消费并写文件,日志内容提前拼好string最安全。

用 chan string 做日志缓冲,主流程只发不等
主流程调用 log.Println 会阻塞直到写入完成,尤其在磁盘慢、日志量大时明显拖慢业务。真正轻量的解法不是换库,而是自己加一层 channel 转发:把日志内容塞进 chan string,后台 goroutine 拿到后才真正写文件。
关键点在于 channel 容量要设——不设就是无缓冲,主流程一发就卡住;设太小又容易丢日志或 panic。建议用带缓冲的 channel,比如 make(chan string, 1024)。
- 日志格式建议提前拼好(如
fmt.Sprintf("[%s] %s", time.Now().Format("2006-01-02 15:04:05"), msg)),避免后台 goroutine 里再做时间格式化,引入锁或误差 - 别在 channel 里传指针或结构体,string 最安全;如果必须传结构,确保它不含 mutex、channel 等不可拷贝字段
- 主流程发日志时,用
select+default防止阻塞:select { case logCh
后台 goroutine 必须处理 close 和 panic
goroutine 读 channel 时,如果主流程退出没关 channel,它会永久阻塞在 ;如果写日志时 panic(比如文件句柄被回收),整个 goroutine 就挂了,后续日志全丢。
所以后台逻辑得包一层 recover,并监听 channel 关闭信号:
- 用
for msg := range logCh自动退出,比for { select { case msg := 更简洁且能响应 close - 每次写文件前检查
*os.File是否有效(file != nil && file.Fd() != ^uintptr(0)),避免 “invalid argument” 错误 - recover 后建议记录一条紧急日志到 stderr,否则问题完全静默:
defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "[FATAL] log writer panic: %v\n", r) } }()
别直接用 os.OpenFile 追加,小心并发写错位
多个 goroutine 同时写同一个文件,即使都用 os.O_APPEND,在某些文件系统(如 ext4 默认)上仍可能因内核 write 缓存导致日志行错乱——两行内容挤在同一行,或中间断开。
解决方法只有一个:所有写操作必须串行。后台 goroutine 是唯一写入者,这就够了;但如果你还开了第二个日志 goroutine(比如同时写 audit.log),就得用不同文件或加锁。
- 打开文件务必加
os.O_SYNC(或调用file.Sync()),否则进程崩溃时最后几条日志大概率丢失 - 避免频繁
os.OpenFile(..., os.O_CREATE|os.O_WRONLY|os.O_APPEND)—— 每次打开都有 syscall 开销;应复用一个*os.File句柄 - 如果需按天轮转,不要在后台 goroutine 里实时判断日期并 reopen;改用信号(如
os.Signal)或定时检查,reopen 动作本身要加锁
用 log.SetOutput 接管标准库日志时注意接口兼容性
想让 log.Printf 也走异步?可以实现 io.Writer,但要注意:标准库 log 在写入时不会检查返回的 n, err,只看 err != nil。如果 channel 满了你返回 err,log 会直接 panic。
所以自定义 Writer 的 Write 方法不能阻塞,也不能轻易报错:
- 返回
len(p), nil即可,哪怕实际没发出去——这是符合io.Writer合约的(“已接收”,不保证“已写出”) - 内部仍要用
select+default投递,失败时记指标或打 warning,别 throw error - 别在
Write里启动新 goroutine,易泄漏;投递逻辑必须复用已有的日志 goroutine
最易被忽略的是:标准库 log 默认加了换行符,你的 channel 消费端如果再拼一次 \n,就会多空行。检查是否重复换行,比优化性能更优先。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











