不能直接用 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 拿到后批量写入文件。
示例关键片段:
立即学习“go语言免费学习笔记(深入)”;
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 <- s + "\n":
default:
// 缓冲满,可选择丢弃或阻塞(去掉 default 就阻塞)
}
}</p><p>func (w *LogWriter) writerLoop() {
buf := make([]byte, 0, 4096)
for {
select {
case line := <-w.ch:
buf = append(buf, line...)
if len(buf) > 8192 { // 达到阈值就刷一次
w.file.Write(buf)
buf = buf[:0]
}
case <-w.done:
if len(buf) > 0 {
w.file.Write(buf)
}
w.file.Close()
return
}
}
}注意:buf 复用避免频繁 alloc;Write 不带 error 检查——实际中应判断返回值并重试/告警;default 分支决定是丢日志还是背压,别漏掉。
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)。


















