log.Println 并非线程安全写文件,因其仅保证内部状态并发安全,而共享 *os.File 写入时 Write 系统调用不原子,会导致日志行交错、截断或乱码。

Go 标准库 log 本身是并发安全的,但直接写文件时若多个 goroutine 共享同一个 *os.File 或未加锁操作 bufio.Writer,仍会触发竞态——go test -race 一定能抓到。
为什么 log.Println 不等于线程安全写文件
标准库 log 的线程安全仅指内部状态(如 prefix、flags)不被破坏;它底层调用 io.WriteString 或 w.Write,而 *os.File.Write 是系统调用,**不保证原子性**。多个 goroutine 同时写同一文件句柄,会导致日志行交错、截断甚至乱码。
- 现象:日志中出现半行文本、两行粘连(如
"INFO: user login" + "ERROR: timeout"变成"INFO: user loginERROR: timeout") - 原因:
Write系统调用返回成功 ≠ 数据已落盘,更不等于“整行原子写入” - 关键点:
log.SetOutput(f *os.File)后所有 goroutine 共享该f,即共享底层 file descriptor
channel 串行化必须配缓冲与独立 writer goroutine
只建一个 chan string 并发发送,却不控制接收端行为,极易卡死或泄漏。核心在于解耦“投递”和“写入”,且写入必须独占文件句柄。
- 缓冲区大小要合理:
make(chan string, 1000)足够应对瞬时高峰,过大(如 10w)会吃光内存,过小(如 1)则发送方频繁阻塞 - 必须用固定数量的 writer goroutine(通常 1 个足够),不能每个日志启一个 goroutine——否则文件句柄竞争照旧
- writer 必须使用
bufio.Writer批量刷盘:w.Flush()前数据还在内存,不调用就可能丢失 - 关闭 channel 前需确保所有日志已投递完毕,否则 writer 会永久阻塞在
range ch
func startLogger(logFile string) (ch chan string, stop func()) {
ch = make(chan string, 1000)
f, _ := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
w := bufio.NewWriter(f)
<pre class="brush:php;toolbar:false;">go func() {
defer f.Close()
defer w.Flush()
for line := range ch {
w.WriteString(line + "\n")
w.Flush() // 关键:不 Flush 就不落盘
}
}()
return ch, func() { close(ch) }}
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
goroutine 闭包捕获变量导致日志内容错乱
这是最隐蔽也最高频的 bug:循环中启动 goroutine,却直接引用循环变量,结果所有日志都记录了最后一次迭代的值。
- 错误写法:
for i := range tasks { go func() { logCh → 全部输出 <code>task 99 done(假设 len=100) - 正确做法:显式传参复制值,
go func(id int) { logCh - 如果用
scanner.Text()读日志行再发进 channel,也必须立即拷贝:line := scanner.Text(); go func(l string) { ch ,否则闭包里读的是 scanner 内部 buffer 的地址,多 goroutine 下内容被覆盖
真正难的不是“怎么串行”,而是确保从日志生成、投递、缓冲、刷盘到文件关闭,每个环节都不逃逸出 channel 模型的约束——尤其是别让任何 goroutine 直接碰 *os.File 或 bufio.Writer。一旦破界,竞态就回来了。

















