
本文详解如何在 Go 中通过多个 goroutine 安全、均匀地消费同一 channel,避免因误用全局 log 包导致所有日志被写入最后一个 worker 文件的问题,并提供线程安全、可维护的日志分发方案。
本文详解如何在 go 中通过多个 goroutine 安全、均匀地消费同一 channel,避免因误用全局 `log` 包导致所有日志被写入最后一个 worker 文件的问题,并提供线程安全、可维护的日志分发方案。
在 Go 中,让多个 goroutine 同时从同一个 channel 接收数据是完全合法且常见的设计(即“扇出”模式),但关键前提是:每个 goroutine 必须持有独立、无共享状态的资源句柄。您当前代码的问题根源不在于 channel 本身,而在于所有 LogWorker 实例都调用了 log.SetOutput() —— 这会覆盖 Go 标准库 log 包的全局默认 logger 输出目标。由于 goroutines 启动存在微小时间差,最后完成 log.SetOutput() 的 worker(通常是第四个)会“赢出”,导致所有后续 log.Println() 调用(无论来自哪个 goroutine)都写入其指定的文件。
✅ 正确做法:为每个 Worker 创建独立 Logger 实例
应摒弃全局 log 包的副作用式配置,改为使用 log.New() 构造专属 logger:
func (lw *LogWorker) Work(evChannel chan Event) {
fmt.Printf("Worker started: %s\n", lw.FileName)
// ✅ 每个 goroutine 持有独立 logger,互不干扰
lg := log.New(&lumberjack.Logger{
Filename: lw.FileName,
MaxSize: lw.MaxSize,
MaxBackups: lw.MaxBackups,
MaxAge: lw.MaxAge,
}, "", 0) // 前缀为空,标志位为 0(禁用时间戳等,默认由 lumberjack 处理)
// ✅ 使用 range 遍历 channel,优雅退出
for event := range evChannel {
lg.Println(Csv(event))
}
}
? 为什么 range 更优?
for event := range evChannel 在 channel 关闭后自动退出循环;而 for { event :=
⚠️ 其他关键注意事项
-
Channel 容量与背压:当前 evChannel = make(chan Event) 是无缓冲 channel。高并发请求下,select 中的 time.After(5 * time.Second) 虽能防阻塞,但可能丢失日志。建议改用带缓冲 channel(如 make(chan Event, 1000))并监控堆积情况。
Worker 生命周期管理:生产环境需支持 graceful shutdown。可在 main() 中添加 signal.Notify() 捕获 SIGTERM,然后关闭 evChannel,使所有 range 循环自然退出。
结构体定义完整性:确保 Event 和 Csv() 函数已正确定义(示例中未给出),且 Csv() 返回 string 类型,避免 lg.Println() 输出异常。
✅ 完整修复要点总结
| 问题点 | 错误实现 | 正确方案 |
|---|---|---|
| 日志输出目标 | 全局 log.SetOutput() 被多次覆盖 | 每个 worker 调用 log.New() 创建独立实例 |
| Channel 消费 | for { | for range ch,支持 channel 关闭语义 |
| 可观测性 | 无启动日志 | fmt.Printf 明确标识 worker 启动 |
| 健壮性 | 无缓冲 channel 易丢日志 | 建议设置合理缓冲区,配合超时丢弃策略 |
通过以上调整,四个 LogWorker 将真正均匀分担日志写入负载,各自写入独立文件(如 event_0, event_1…),彻底解决“只看到最后一个文件”的问题,同时符合 Go 并发编程的最佳实践。











