
Go 中多个 goroutine 同时调用 fmt.Print 可能导致输出内容交错(interleaving),虽在单线程模式下不易复现,但在多核并发场景下真实存在,需通过同步机制或专用日志工具保障输出一致性。
go 中多个 goroutine 同时调用 `fmt.print` 可能导致输出内容交错(interleaving),虽在单线程模式下不易复现,但在多核并发场景下真实存在,需通过同步机制或专用日志工具保障输出一致性。
在 Go 程序中,fmt.Print、fmt.Println 等函数默认操作的是标准输出(os.Stdout),而 os.Stdout 本身不是线程安全的——它底层对应一个文件描述符,写入操作由操作系统调度,但 Go 运行时并不为每次 fmt 调用自动加锁。这意味着:当多个 goroutine 并发执行 fmt.Print("ABC") 时,若各自写入未完成即被调度切换,就可能产生字符级交错。
例如以下代码看似简单,实则隐患明显:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 强制启用多 OS 线程以增大竞态概率
runtime.GOMAXPROCS(2)
go func() {
for i := 0; i < 100; i++ {
fmt.Print("A")
time.Sleep(1 * time.Nanosecond) // 微小延迟加剧调度不确定性
}
}()
go func() {
for i := 0; i < 100; i++ {
fmt.Print("B")
time.Sleep(1 * time.Nanosecond)
}
}()
time.Sleep(10 * time.Millisecond)
}多次运行后,你可能观察到类似 ABABABAAABBBBBAAB... 的非预期混合输出(尤其在重定向到终端或管道时更易暴露)。这是因为:
- fmt.Print 内部先格式化字符串,再调用 os.Stdout.Write();
- Write() 是系统调用,不保证原子性;短字符串可能被拆分为多次 write(如内核缓冲区满、信号中断等);
- stderr 比 stdout 更危险:因默认无缓冲,每次 fmt.Fprintln(os.Stderr, ...) 几乎都触发一次系统调用,交错概率显著升高。
✅ 正确做法有三类:
1. 使用互斥锁(Mutex)串行化输出
适用于自定义调试输出或轻量日志:
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
func safePrint(s string) {
mu.Lock()
defer mu.Unlock()
fmt.Print(s)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
safePrint(fmt.Sprintf("G%d:%d ", id, j))
}
}(i)
}
wg.Wait()
fmt.Println() // 换行
}2. 使用标准库 log 包
log.Logger 内置了 sync.Mutex 和缓冲区,所有方法(Print/Printf/Fatal)天然并发安全:
package main
import (
"log"
"os"
)
func main() {
logger := log.New(os.Stdout, "", 0)
go func() { logger.Println("From goroutine A") }()
go func() { logger.Println("From goroutine B") }()
// 输出严格按完整行隔离,无字符交错
}3. 避免在 hot path 中直接打印
生产环境应禁用 fmt.Print* 做日志,改用结构化日志库(如 zap、zerolog),它们不仅线程安全,还支持异步写入、采样、字段注入等工业级特性。
⚠️ 注意事项:
- 不要依赖 GOMAXPROCS=1 规避问题——这只是掩盖竞态,而非解决;
- fmt.Printf 格式化本身是内存操作,安全;但最终 Write() 到 os.Stdout 才是风险点;
- 即使使用 io.WriteString(os.Stdout, s),同样不安全,因其未加锁;
- 若需高性能且带格式的日志,优先封装 log.Logger 或选用 zap(其 Sugar 模式兼顾简洁与性能)。
总结:Go 的并发模型赋予开发者强大能力,但也要求主动管理共享资源。控制台输出作为典型的共享 I/O 资源,必须显式同步。选择 log 包是最简、最可靠、最符合 Go 风格的实践方案。











