
在 Go 中,多个 goroutine 直接调用 fmt.Print 等标准输出函数时,可能产生交错输出(interleaving),导致日志或控制台信息混乱;这不是理论风险,而是在 GOMAXPROCS > 1、高并发或写入 stderr 等场景下真实可复现的问题。
在 go 中,多个 goroutine 直接调用 `fmt.print` 等标准输出函数时,**可能产生交错输出(interleaving)**,导致日志或控制台信息混乱;这不是理论风险,而是在 `gomaxprocs > 1`、高并发或写入 stderr 等场景下真实可复现的问题。
Go 的标准输出(如 os.Stdout)本身不是 goroutine 安全的。虽然 fmt.Print 系列函数内部对底层 io.Writer 的写入做了简单封装,但 os.Stdout.Write 操作本身是系统调用,不保证原子性——尤其当多个 goroutine 同时调用 fmt.Print("ABC") 和 fmt.Print("XYZ") 时,底层可能将 "AB"、"XY"、"C"、"Z" 等字节片段交错写入终端缓冲区,最终呈现为 ABXYZC 这类不可读的混合结果。
你提供的示例看似“正常”,是因为:
- 默认 GOMAXPROCS=1(Go 1.5+ 后默认为 CPU 核心数,但小规模测试常受调度影响);
- "ABCDEF" 较短,stdout 缓冲(行缓冲或全缓冲)可能快速吞吐,掩盖了竞争;
- stderr 更危险:它通常无缓冲,每次 fmt.Fprintln(os.Stderr, ...) 都触发独立系统调用,交错概率显著升高。
✅ 验证交错现象的可靠方式:
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(4) // 显式启用多线程调度
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
fmt.Printf("[G%d]%d ", id, j) // 短字符串 + 空格,易暴露交错
}
}(i)
}
wg.Wait()
fmt.Println()
}多次运行该程序,你很可能观察到类似 [G3]42 [G7]15[G3]43 的乱序片段——这正是竞态的直接证据。
✅ 正确解决方案
1. 使用 log 包(推荐用于日志)
log.Logger 内置互斥锁和缓冲区,天然支持并发安全:
package main
import (
"log"
"os"
)
func main() {
logger := log.New(os.Stdout, "", log.LstdFlags)
// 多个 goroutine 可安全调用
go func() { logger.Println("from goroutine A") }()
go func() { logger.Println("from goroutine B") }()
// ...
}2. 手动加锁(适用于自定义格式或非日志场景)
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
func safePrintln(v ...any) {
mu.Lock()
defer mu.Unlock()
fmt.Println(v...)
}
func main() {
go func() { safePrintln("Hello from G1") }()
go func() { safePrintln("Hello from G2") }()
// 确保主线程等待(生产环境应使用 sync.WaitGroup)
select {}
}3. 使用带缓冲的 io.Writer + 单独 writer goroutine(高吞吐场景)
适用于需极致性能且允许轻微延迟的日志系统(如将日志统一发送至 channel,由单个 goroutine 序列化写入)。
⚠️ 注意事项
- 不要依赖 fmt.Printf 的“看起来正常”来判断线程安全性——这是典型的偶发竞态(heisenbug),压力测试才能暴露;
- fmt.Print* 函数族(Print, Println, Printf)均不保证并发安全,即使目标是 os.Stdout;
- 若需结构化日志(JSON、带字段),务必使用 log 或成熟库(如 zap, zerolog),它们不仅线程安全,还避免反射开销;
- 在调试阶段,可临时设置 GODEBUG=schedtrace=1000 观察调度行为,辅助定位输出紊乱是否源于 goroutine 调度竞争。
总之,并发打印不是“是否可能”的问题,而是“何时必然发生”的问题。从项目第一天起,就应将输出同步视为基础设施需求,而非事后补救项。











