为什么并发日志在网站开发中很重要
做网站的人应该都遇到过这种情况:系统一到高峰期,日志要么丢失,要么写入延迟,甚至拖垮整个服务。特别是用 Go 写的后端服务,天生支持高并发,成千上万个 goroutine 同时运行,如果日志处理不当,很容易变成性能瓶颈。
比如你上线了一个促销页面,瞬间涌入大量用户,每个请求都想记一条日志。如果直接用 fmt.Println 或者简单的文件写入,多个协程同时操作 I/O,轻则日志错乱,重则文件锁死、程序卡住。
避免并发写日志的常见坑
最直接但错误的方式是让每个 goroutine 直接往同一个文件写日志:
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
file.WriteString("[INFO] 用户登录\n")问题来了:多个协程同时调 file.WriteString,操作系统层面可能会出现写入交错,日志内容混在一起,比如两条日志拼成一条,或者部分内容被覆盖。
使用通道(channel)集中处理日志
一个更稳妥的做法是把日志写入操作收口,通过一个专门的“日志协程”来统一处理。其他协程只负责发送日志消息,不直接操作文件。
可以定义一个日志通道,用来接收所有日志条目:
var logChan = make(chan string, 1000)
func init() {
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
go func() {
for msg := range logChan {
file.WriteString(msg + "\n")
}
}()
}业务代码里只需要发消息:
logChan <- "[WARN] 支付超时,订单ID: 12345"这样所有写操作都由单个协程完成,避免了并发冲突,也提高了写入效率。
借助第三方库简化实现
虽然自己实现通道方案能理解原理,但在真实项目中,更推荐用成熟的日志库,比如 zap 或 logrus,它们内部已经处理好了并发安全的问题。
以 zap 为例,它不仅线程安全,还特别快:
logger, _ := zap.NewProduction()
defer logger.Sync() // 确保日志刷到磁盘
// 多个 goroutine 可以同时调用
logger.Info("用户注册成功",
zap.String("email", "user@example.com"),
zap.Int("userID", 1001))zap 的设计就是为高并发准备的,结构化日志输出也很适合后续收集分析。
结合文件轮转避免日志过大
光写进一个文件也不行,时间一长文件动辄几个G,查看困难,还可能占满磁盘。可以用 lumberjack 配合 zap 实现自动切分:
writer := &lumberjack.Logger{
Filename: "/var/log/myapp.log",
MaxSize: 10, // 每 10MB 轮转一次
MaxBackups: 5, // 最多保留 5 个旧文件
MaxAge: 7, // 最多保存 7 天
Compress: true,
}
logger := zapcore.AddSync(writer)这样一来,既保证了并发安全,又解决了运维层面的日志管理问题。
实际搭建网站后端时,别小看日志这一环。选对方法,系统才能稳得住,出问题也更容易排查。