
在 Go 中,多个 goroutine 并发调用 fmt.Print 等标准输出函数时,可能产生输出交错(interleaving),导致字符混杂、日志错乱;这不是竞态检测器能捕获的问题,但真实存在且需显式同步。
在 go 中,多个 goroutine 并发调用 `fmt.print` 等标准输出函数时,**可能产生输出交错(interleaving)**,导致字符混杂、日志错乱;这不是竞态检测器能捕获的问题,但真实存在且需显式同步。
Go 的标准输出(os.Stdout)本身不是 goroutine 安全的写入目标。虽然 fmt.Print 系列函数内部对单次调用做了原子性封装(例如一次 Write 系统调用),但无法保证跨 goroutine 的多次打印操作不被中断——尤其当输出内容较长、GOMAXPROCS > 1、或写入目标为无缓冲的 os.Stderr 时,交错风险显著上升。
以下代码直观演示了潜在问题:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(2) // 显式启用多线程调度
go func() {
for i := 0; i < 100; i++ {
fmt.Print("ABC")
time.Sleep(1 * time.Microsecond) // 增加调度机会
}
}()
for i := 0; i < 100; i++ {
fmt.Print("XYZ")
time.Sleep(1 * time.Microsecond)
}
}运行多次后,你可能观察到类似 ABXYZCABXYZC... 或 ABABXYZCXYZ... 的混杂输出——这正是两个 goroutine 的 Write 系统调用被操作系统/运行时交错调度所致。
⚠️ 注意事项:
- fmt.Print 的“看似安全”源于 stdout 的行缓冲(在终端中)及短字符串写入速度较快,但这属于偶然,不可依赖;
- os.Stderr 默认无缓冲,交错更易复现;
- go run -race 不会报告此类问题,因为竞态检测器仅检查共享内存访问,而 os.Stdout.Write 是系统调用,不涉及 Go 层面的变量竞争;
- 标准库 log 包之所以安全,正因其内部使用了 sync.Mutex + 缓冲写入(见 log.Logger 源码)。
✅ 推荐解决方案:
-
使用 log 包替代裸 fmt.Print(最简单可靠)
import "log" var logger = log.New(os.Stdout, "", 0) go func() { logger.Println("goroutine A: started") }() logger.Println("main: running") -
手动加锁(适用于需自定义格式或避免 log 前缀场景)
var mu sync.Mutex func safePrint(a ...any) { mu.Lock() defer mu.Unlock() fmt.Print(a...) } 集中式日志协程(高吞吐、低延迟场景)
通过 channel 收集日志消息,由单一 goroutine 顺序写入,兼顾解耦与安全性。
总结:并发输出不是“是否可能交错”的问题,而是“何时必然交错”的问题。永远不要假设 fmt.Print 在多 goroutine 下是线程安全的。 正确做法是:优先使用 log 包;若需精细控制,请显式同步——这是编写健壮 Go 服务的基本素养。










