发布于2026-07-18 阅读(0)
扫一扫,手机访问
在 Go 中使用多个 goroutine 并发消费同一个 channel,这本是日常开发里的常规操作,但稍不留神就容易翻车——尤其是跟共享状态打交道的时候。你碰到的那个怪现象:“所有日志只写进了第四个文件”,真相并不是 channel 分发逻辑出了岔子,而是对 Go 标准库 log 包的使用踩了坑。
问题出在哪?log.SetOutput() 和 log.SetFlags() 这两个方法,修改的是全局、包级的默认 logger(也就是 log.Default() 返回的那个实例)。当 4 个 LogWorker.Work() 方法依次跑起来之后,每个方法都会调一次 log.SetOutput(...),把全局 logger 的输出目标换成自己的 lumberjack.Logger。最后执行的那个 goroutine(通常是第 4 个)的设置会留下来,覆盖掉前面三个。其余 goroutine 虽然也在老老实实地调 log.Println(...),但实际输出的目标早就被最后那个覆盖成了同一个——第四个文件。这就是为什么你只看到一个日志文件里有内容,其余三个文件干干净净。
✅ 正确的解决方案其实不复杂:给每个 goroutine 创建一个专属的 *log.Logger 实例,别再动全局 logger 了。把 LogWorker.Work 方法改成下面这样:
func (lw *LogWorker) Work(evChannel <-chan Event) {
fmt.Printf("LogWorker started: %s\n", lw.FileName)
// ✅ 为本 goroutine 创建独立 logger
lg := log.New(&lumberjack.Logger{
Filename: lw.FileName,
MaxSize: int64(lw.MaxSize) * 1024 * 1024, // 注意:lumberjack.MaxSize 单位是字节
MaxBackups: lw.MaxBackups,
MaxAge: lw.MaxAge,
}, "", log.LstdFlags) // 可选:加个时间戳之类的标志
// ✅ 使用 range 遍历 channel,优雅退出
for event := range evChannel {
lg.Println(Csv(event))
}
}
这段代码有四个关键改动值得注意:
log.New(...) 返回一个全新的实例,每个 goroutine 各持一份,互不干扰,彻底切断全局状态的污染。<-chan Event(只接收通道),语义更清晰,也防止误写操作。for event := range evChannel 在 channel 关闭后会自动结束循环,避免 for {} 空转;写法也更符合 Go 的惯用风格。lumberjack.MaxSize 接收的是字节数,原代码里传的是 int(MB),需要转成字节 int64(lw.MaxSize) * 1024 * 1024,否则日志轮转可能会失效。⚠️ 还有几个细节需要留意:
evChannel。生产环境中,建议在服务关闭前(比如收到 SIGINT 信号时)显式关闭 channel,通知所有 worker 退出。可以配合 sync.WaitGroup 等待 worker 结束。Csv(event) 有可能 panic 或返回 error,那在 lg.Println() 之前最好加一层防御性检查。lumberjack.Logger 本身是线程安全的,不需要额外加锁。但如果 CSV 序列化开销比较大,可以考虑在 worker 内部做批处理——比如先缓冲 N 条记录,再一次性写入。经过上面这番改造,4 个 LogWorker 就能真正并行、独立地消费 evChannel,各自把日志写到指定的文件里(比如 event_0、event_1……),实现负载分散和日志隔离,再也不会互相干扰了。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8