不能直接用 os.file.write 写日志,因其同步写磁盘会阻塞主线程,高并发时可能卡住几十毫秒;需通过 channel 队列+单 goroutine 串行落盘,控制缓冲区上限、复用 buf、确保 flush,兼顾不丢日志、内存可控与主流程性能。

为什么不能直接用 os.File.Write 写日志
同步写磁盘会卡住主线程,尤其在高并发或频繁打日志时,Write 可能阻塞几十毫秒甚至更久。Go 的 log 包默认就是同步写,不加改造没法扛住真实业务流量。
真正要解决的不是“怎么异步”,而是“怎么避免丢日志 + 控制内存 + 不拖慢主流程”。常见错误是只起 goroutine 丢 Write,结果日志堆积、OOM 或 panic:比如多个 goroutine 同时写同一个 *os.File(未加锁),或者缓冲区无限增长。
- 必须串行化写入:同一文件只能由一个 goroutine 负责落盘
- 缓冲区要有硬上限,超限时要么丢日志、要么阻塞生产者(根据业务容忍度选)
- 程序退出前要调用
Flush,否则最后一段日志就丢了
用 chan 做日志队列 + 单写 goroutine
这是最轻量、可控性最强的方式。核心是把日志条目(字符串或结构体)发到 channel,后台 goroutine 拿到后批量写入文件。
示例关键片段:
type LogWriter struct {
ch chan string
file *os.File
done chan struct{}
}
<p>func NewLogWriter(path string) (*LogWriter, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
w := &LogWriter{
ch: make(chan string, 1000), // 缓冲区大小按需设(如 1k 条)
file: f,
done: make(chan struct{}),
}
go w.writerLoop()
return w, nil
}</p><p>func (w *LogWriter) Write(s string) {
select {
case w.ch </p><p>func (w *LogWriter) writerLoop() {
buf := make([]byte, 0, 4096)
for {
select {
case line := 8192 { // 达到阈值就刷一次
w.file.Write(buf)
buf = buf[:0]
}
case 0 {
w.file.Write(buf)
}
w.file.Close()
return
}
}
}</p>
注意:buf 复用避免频繁 alloc;Write 不带 error 检查——实际中应判断返回值并重试/告警;default 分支决定是丢日志还是背压,别漏掉。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
io.Writer 接口兼容与性能取舍
如果想无缝替换标准库 log.SetOutput,你的类型必须实现 io.Writer。但要注意:Write([]byte) 方法会被高频调用,每次调用都发 channel 会放大调度开销。
- 不要在
Write里直接select { case ch —— 字节切片转字符串有拷贝成本 - 更稳妥的做法是:在
Write中 copy 到本地 buffer,攒够再发 channel(类似上面的writerLoop逻辑) - 若追求极致吞吐,可用
sync.Pool管理临时 buffer,但小项目没必要
另外,os.File 本身已带内核缓冲,所以不必过度优化“小 write 合并”——重点是控制用户态缓冲大小和刷新时机。
关闭时务必等写完,否则日志丢失
很多人忘了 Close 或没等 goroutine 结束,导致进程退出时 channel 还有积压、goroutine 被强制杀死。
正确做法:
- 暴露
Close()方法,向donechannel 发信号,并close(ch) - 在
writerLoop里消费完 channel 才退出(用for line := range w.ch) - 主流程调用
Close()后,可加简单等待(如time.Sleep(10ms)),或用sync.WaitGroup更严谨
最易被忽略的是:没关文件描述符,导致重启时报 too many open files;或者用 defer w.Close() 却没确保它真被执行(比如 panic 早于 defer)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










