go标准库log包默认输出到os.stderr,高并发下因频繁系统调用、加锁和格式化易成瓶颈;应采用chan+goroutine异步写入,预格式化、设合理缓冲、加超时控制。

为什么不用 log 包直接写文件
Go 标准库 log 默认输出到 os.Stderr,即使重定向到文件,也会在每次调用时触发系统调用、加锁、格式化,高并发下容易成为瓶颈。真实场景中,日志写入延迟突增、goroutine 阻塞、磁盘 I/O 拖垮主逻辑,往往就源于没做缓冲和异步解耦。
关键不是“能不能用”,而是“扛不扛得住每秒几千条日志”。本地日志库的核心诉求其实是:低延迟、不阻塞业务、可滚动、线程安全、可控内存占用。
用 chan + goroutine 做异步写入
最轻量又有效的解法是把日志条目投进一个带缓冲的 chan,由单独 goroutine 持续消费并批量刷盘。避免每个 log.Println() 都同步调用 Write()。
实操建议:
- 缓冲区大小设为 1024~8192(
make(chan *LogEntry, 4096)),太小易丢日志,太大吃内存 - 消费 goroutine 要用
for range持续读取,别漏掉close()后的退出逻辑 - 不要在消费端做复杂格式化——格式化提前在生产端完成,只传
string或预分配的[]byte - 务必加超时控制,防止
Write()卡死导致 channel 堆积,可用select { case 做兜底
按大小滚动 + 文件句柄复用
滚动不是靠定时器轮询,而是写入前检查当前文件大小。用 os.Stat() 开销小,比 os.Getwd() + 字符串拼接更可靠;句柄始终复用 *os.File,避免反复 OpenFile(..., os.O_APPEND) 导致 fd 耗尽。
常见错误:
- 滚动时只 rename 不 close 原文件 → 句柄泄漏,Linux 下
lsof -p PID | grep your-log一眼可见 - 用
os.Create()新建文件 → 权限丢失(比如期望 0644 却变成 0600) - 没处理
ErrInvalid或syscall.ENOSPC→ 磁盘满时 panic 或静默丢日志
推荐写法:f, _ := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644),滚动时先 f.Close() 再 os.Rename(),新日志继续用新 OpenFile 句柄。
时间戳、级别、Caller 的低成本获取
time.Now() 是性能热点,别在每条日志里都调;runtime.Caller() 开销更大,只在 debug 模式开启。实操上:
- 用
sync.Pool复用time.Time对应的字符串缓存(如"2024-05-22T14:03:15.123"),避免频繁fmt.Sprintf - 日志级别用 iota 枚举(
LevelDebug=0, LevelInfo=1...),打印时查表转字符串,比 if-else 快 - caller 信息提取放在异步 goroutine 消费阶段,并加
if l.level 条件开关 - 避免用
fmt.Sprint拼接字段,改用bytes.Buffer或预分配[]byte+strconv.AppendInt等零分配操作
真正压测下来,50% 的耗时可能花在字符串拼接和内存分配上,而不是磁盘写入本身。
滚动策略、异步通道、时间缓存这些点单独看都很简单,但组合起来后,哪一环少做一次 Close()、多一次 fmt、忘了池化,就会在 QPS 上千时突然掉链子。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











