fmt.Println在goroutine中乱序是因为os.Stdout.Write非并发安全,多goroutine竞争写入导致字符混杂或行序颠倒;应使用sync.Mutex保护或channel汇总输出。

为什么 fmt.Println 在 goroutine 里会乱序
Go 的 fmt.Println 本身不是并发安全的——它底层调用的是 os.Stdout.Write,而标准输出是共享的文件描述符。多个 goroutine 同时写入时,系统调用可能交错,导致行内字符混杂(比如 “HeWorld\nll”)或整行顺序颠倒。这不是 Go 的 bug,而是未加同步的典型竞态表现。
常见错误现象:
• 输出内容跨行粘连(如两行合并成一行)
• 行序完全不可预测,每次运行结果不同
• 单行内文字被其他 goroutine 插入片段
- 不要用
runtime.Gosched()或time.Sleep“凑巧” 排序——这既不可靠,也不解决根本问题 - 避免在打印前用
sync.WaitGroup等待全部 goroutine 结束再统一输出——这丧失了并发意义,且无法体现实时性 - 真正需要的是:写操作原子化,或输出路径隔离
用 sync.Mutex 保护标准输出最直接
如果只是想让多 goroutine 打印“看起来有序”,且不介意串行化写入开销,sync.Mutex 是最易理解、最少侵入的方案。
关键点:
• 锁必须包裹整个 fmt.Println 调用,不能只锁字符串拼接
• 不要复用全局 log.Logger 实例而不加锁——它的 Output 方法仍会并发写 os.Stderr
立即学习“go语言免费学习笔记(深入)”;
var mu sync.Mutexfunc safePrint(v ...interface{}) { mu.Lock() defer mu.Unlock() fmt.Println(v...) }
// 在 goroutine 中调用 go func() { safePrint("task", 1, "done") }()
用 channel 汇总日志比锁更符合 Go 风格
当打印频率高、或需做日志分级/缓冲/落盘时,channel 方案更健壮。它把“产生日志”和“输出日志”解耦,天然避免竞争。
使用场景:
• 需要控制输出节奏(如限流、批量 flush)
• 日志要同时输出到终端 + 文件 + 网络
• 希望避免某个 slow output(如网络写)阻塞业务 goroutine
- 定义带缓冲的 channel(如
chan string),容量建议 ≥ 并发 goroutine 数 × 2 - 启动一个专用 goroutine 从 channel 读取并
fmt.Println,它独占输出权 - 业务 goroutine 只负责发送,无锁、无等待(除非 channel 满)
logCh := make(chan string, 100)
go func() {
for msg := range logCh {
fmt.Println(msg)
}
}()
// 使用
go func() {
logCh <- "worker 3 finished"
}()
什么时候不该强制同步输出
强行让并发程序的打印“按启动顺序出现”,往往暴露了设计偏差。真实服务中,goroutine 完成时间本就不确定,按执行完成顺序输出才合理。
容易被忽略的点:
• fmt.Printf 和 fmt.Println 在并发下行为一致,别以为换函数能绕过问题
• 如果用 log 包,默认也是非线程安全的;要用 log.SetOutput 配合 mutex 或自定义 writer
• 在测试中依赖打印顺序断言,本质是在测调度器行为,极不稳定
复杂点在于:同步只是表象,真正要问的是——你到底需要“可读性”“可调试性”,还是“语义顺序保证”?前者用 channel 汇总就够了,后者通常得靠业务层序列号或时间戳标注,而不是靠 stdout 写入顺序。











