
在 Go 中,多个 goroutine 同时调用 fmt.Print 等标准输出函数可能导致输出内容交错(interleaving),因标准 I/O 并非协程安全;需通过同步机制(如互斥锁、通道或 log 包)确保输出原子性。
在 go 中,多个 goroutine 同时调用 `fmt.print` 等标准输出函数可能导致输出内容交错(interleaving),因标准 i/o 并非协程安全;需通过同步机制(如互斥锁、通道或 `log` 包)确保输出原子性。
Go 的标准库 fmt 包中所有输出函数(如 fmt.Print, fmt.Println, fmt.Printf)本身不提供协程安全保证。当多个 goroutine 并发写入 os.Stdout(或 os.Stderr)时,底层系统调用 write() 可能被抢占或调度,导致字节流交错——即使单次 fmt.Print("ABCDEF") 在逻辑上是“一个操作”,其实际执行仍可能拆分为多次系统调用(尤其在高并发、多 OS 线程(GOMAXPROCS > 1)、或写入未缓冲的 stderr 时)。你提供的示例虽在本地偶现“正常”,但这是侥幸而非保证:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// 启动 10 个竞争 goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
fmt.Print("[", id, "]ABCDEF\n") // 非原子!易交错
}
}(i)
}
wg.Wait()
}运行该程序(建议设置 GOMAXPROCS=4 并重定向到终端或文件),极大概率观察到类似 [0]AB[1]CDEF\n[0] 的乱序输出。
✅ 正确做法:显式同步输出
方案 1:使用 sync.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 < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
safePrint(fmt.Sprintf("[%d]ABCDEF\n", id))
}
}(i)
}
wg.Wait()
}✅ 优势:轻量、可控;❌ 注意:避免在 Lock() 内执行耗时操作(如网络请求),以防阻塞其他 goroutine。
方案 2:使用标准库 log 包(推荐用于日志场景)
log.Logger 内置互斥锁与缓冲区,天然协程安全:
package main
import (
"log"
"os"
)
func main() {
logger := log.New(os.Stdout, "", 0) // 无前缀、无标志
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
logger.Printf("[%d]ABCDEF", id) // 安全!自动加锁
}
}(i)
}
wg.Wait()
}方案 3:通过 channel 实现串行化(适合复杂格式化逻辑)
type LogMsg struct{ Text string }
var logCh = make(chan LogMsg, 100) // 缓冲通道防阻塞
func init() {
go func() {
for msg := range logCh {
fmt.Print(msg.Text)
}
}()
}
func asyncPrint(s string) {
logCh <- LogMsg{s} // 非阻塞发送(若缓冲满则可能阻塞,需按需调整容量)
}⚠️ 关键注意事项
- 不要依赖 fmt 的“看起来正常”:输出是否交错取决于调度时机、缓冲区状态、OS 写入行为,具有不确定性;
- os.Stdout 默认行缓冲(终端)或全缓冲(重定向到文件),而 os.Stderr 无缓冲,更易暴露竞态;
- 若需高性能结构化日志,优先选用 log/slog(Go 1.21+)或成熟第三方库(如 zerolog, zap),它们均内置并发安全设计;
- 单元测试中可通过捕获 os.Stdout 并检查输出完整性来验证同步逻辑,但无法 100% 覆盖竞态边界。
总之,并发 I/O 必须显式同步——Go 的并发模型强调“共享内存通过通信”,但标准输出是共享的底层资源,通信(channel)或同步原语(mutex)是保障一致性的必要手段。











