
在 Go 中,多个 goroutine 直接调用 fmt.Print 等标准输出函数时,可能发生输出内容交错(interleaving),导致日志或调试信息混乱;这不是概率极低的偶发问题,而是由底层 I/O 非原子性及无锁设计决定的确定性风险。
在 go 中,多个 goroutine 直接调用 `fmt.print` 等标准输出函数时,**可能发生输出内容交错(interleaving)**,导致日志或调试信息混乱;这不是概率极低的偶发问题,而是由底层 i/o 非原子性及无锁设计决定的确定性风险。
Go 的 fmt 包(如 fmt.Print, fmt.Println, fmt.Printf)本身不提供并发安全保证。其底层通过 os.Stdout.Write() 写入,而该系统调用在多线程环境下(即 GOMAXPROCS > 1 且存在多个活跃 goroutine)可能被调度器中断——尤其当输出内容较长、或目标为无缓冲的 os.Stderr 时,交错现象极易复现。
例如,以下代码看似简单,实则存在竞态:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(2) // 显式启用多线程调度
for i := 0; i < 5; i++ {
go func(id int) {
for j := 0; j < 3; j++ {
fmt.Print("[G", id, "]", "HELLO\n")
time.Sleep(1 * time.Microsecond) // 增加调度干扰概率
}
}(i)
}
time.Sleep(10 * time.Millisecond)
}运行多次后,你很可能看到类似这样的输出:
[G0]HELLO [G1][G0]HELLO [G1]HELLO [G2]HELLO [G2]HELLO [G3]HELLO[G1]HELLO
⚠️ 注意:
- 即使 GOMAXPROCS=1(默认单 OS 线程),goroutine 仍可被抢占(如遇 time.Sleep、channel 操作、系统调用等),不能依赖此设置保证输出安全;
- os.Stdout 虽有缓冲,但缓冲行为不可控且非原子:一次 fmt.Print("ABC") 可能拆分为多次 Write() 系统调用,中间即可能被其他 goroutine 插入;
- os.Stderr 默认无缓冲,交错风险更高,绝不应直接用于多 goroutine 日志输出。
✅ 正确做法:引入同步机制,确保写入操作的原子性。推荐三种生产级方案:
1. 使用 log 标准库(最推荐)
log.Logger 内置互斥锁和缓冲区,天然并发安全,且支持前缀、时间戳、分级等能力:
package main
import (
"log"
"os"
"time"
)
var logger = log.New(os.Stdout, "[INFO] ", log.LstdFlags)
func main() {
for i := 0; i < 3; i++ {
go func(id int) {
for j := 0; j < 2; j++ {
logger.Printf("task %d processed item %d", id, j)
time.Sleep(1 * time.Microsecond)
}
}(i)
}
time.Sleep(10 * time.Millisecond)
}
// 输出始终是完整、有序的行,无任何交错2. 手动加锁(适合定制化场景)
若需完全控制输出格式(如彩色 ANSI 码、实时 flush),可封装带 sync.Mutex 的 writer:
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
func safePrint(a ...interface{}) {
mu.Lock()
defer mu.Unlock()
fmt.Print(a...)
}
func main() {
for i := 0; i < 2; i++ {
go func(id int) {
for j := 0; j < 3; j++ {
safePrint("G", id, ": ", j, "\n")
}
}(i)
}
// 注意:此处需同步等待,否则主 goroutine 可能提前退出
// 实际项目中建议使用 sync.WaitGroup
}3. 专用日志通道(高吞吐场景)
对性能敏感服务,可采用「生产者-消费者」模型,将日志统一发送至 channel,由单个 goroutine 序列化写入:
type LogEntry struct{ Msg string }
var logCh = make(chan LogEntry, 100)
func init() {
go func() {
for entry := range logCh {
fmt.Print(entry.Msg)
}
}()
}
func asyncLog(msg string) {
select {
case logCh <- LogEntry{Msg: msg}:
default:
// 队列满时降级处理(如丢弃或 panic)
fmt.Fprint(os.Stderr, "[WARN] log dropped\n")
}
}? 总结:
- *永远不要假设 `fmt.Print` 是并发安全的**——它不是,且文档明确未作此承诺;
- 优先选用 log 包,它经过充分测试、轻量、可配置,是 Go 官方推荐的日志方案;
- 若需极致控制,请显式同步(锁/通道),并注意避免死锁与性能瓶颈;
- 在调试阶段可通过 GOMAXPROCS=2 + time.Sleep 主动诱发交错,验证同步逻辑是否生效。
安全的并发输出,不是靠“运气看起来正常”,而是靠明确的同步契约。











