Go 中安全配置多个日志协程方法
本文详解为何多个goroutine共享log.SetOutput()会导致日志全部写入最后一个文件,并提供基于独立log.Logger实例的正确实现方案,确保每个LogWorker写入专属日志文件。

本文详解为何多个 goroutine 共享 log.SetOutput() 会导致日志全部写入最后一个文件,并提供基于独立 log.Logger 实例的正确实现方案,确保每个 LogWorker 写入专属日志文件。
本文详解为何多个 goroutine 共享 `log.SetOutput()` 会导致日志全部写入最后一个文件,并提供基于独立 `log.Logger` 实例的正确实现方案,确保每个 LogWorker 写入专属日志文件。
问题根源在于:Go 标准库的 log 包使用全局变量管理默认 logger(log.std),而 log.SetOutput() 和 log.SetFlags() 会直接修改该全局实例。当多个 LogWorker.Work() 并发调用 log.SetOutput(&lumberjack.Logger{...}) 时,后执行的 goroutine 会覆盖前者的输出目标——最终所有 log.Println() 调用都流向最后一个设置的 lumberjack.Logger,导致仅生成一个日志文件(如 event_3),其余 worker 彻底失效。
✅ 正确做法是为每个 worker 创建*独立的 `log.Logger实例**,而非复用全局 logger。修改LogWorker.Work` 方法如下:
func (lw *LogWorker) Work(evChannel <-chan Event) {
fmt.Printf("LogWorker started: %s\n", lw.FileName)
// ✅ 为每个 worker 创建专属 logger,避免全局污染
lg := log.New(&lumberjack.Logger{
Filename: lw.FileName,
MaxSize: lw.MaxSize,
MaxBackups: lw.MaxBackups,
MaxAge: lw.MaxAge,
}, "", 0) // prefix 为空,flag 为 0(不加时间戳等)
// ✅ 使用 range 遍历通道,支持优雅关闭
for event := range evChannel {
lg.Println(Csv(event))
}
}关键改进点说明:
- 独立 logger 实例:log.New(...) 返回新 *log.Logger,各 worker 持有完全隔离的日志输出对象,互不影响;
- 只读通道参数:将 evChannel chan Event 改为 evChannel <-chan Event,明确语义为“仅接收”,提升代码可读性与安全性;
- range 替代 for { <-ch }:当通道被关闭(例如程序退出时需清理),range 会自动退出循环,避免 goroutine 泄漏;而无限 for { <-ch } 在关闭通道后将持续接收零值并 panic(若未检查 ok);
- 移除 log.SetFlags(0) 的副作用:该调用影响全局 logger,已无必要;专属 logger 的 flag 在 log.New() 中直接指定。
? 补充建议:
- 在 main() 中,可通过 sync.WaitGroup 管理 worker 生命周期,确保服务关闭前所有日志落盘;
- 若需动态增减 worker 数量,应使用带缓冲的 channel 或结合 select + default 实现非阻塞写入,防止请求因日志队列满而丢失;
- 生产环境建议增加日志错误处理(如 lg.Output() 返回 error 时告警),并考虑结构化日志库(如 zap)替代 log + lumberjack 组合以获得更高性能与灵活性。
通过以上重构,四个 LogWorker 将严格按预期分别写入 event_0, event_1, event_2, event_3 四个独立文件,真正实现并发日志分流。


































