
go 语言中,向无缓冲通道发送数据的 goroutine 之间不存在内置调度优先级或轮转机制,多个发送者并发写入同一通道时,其执行顺序是不确定的,取决于调度器随机选择,因此无法保证“ping”和“pong”严格交替输出。
在您提供的示例中,pinger 和 ponger 两个 goroutine 独立、无协调地持续向同一个无缓冲 channel c 发送字符串:
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping" // 阻塞直到有接收者就绪
}
}
func ponger(c chan string) {
for i := 0; ; i++ {
c <- "pong" // 同样阻塞等待
}
}由于 printer 是唯一接收者,每次仅能接收一个值,而 pinger 和 ponger 在 c 发送阻塞状态,等待 printer 执行 同时被唤醒竞争通道所有权——此时 Go 调度器(runtime scheduler)会非确定性地选择其中一个完成发送。该行为不依赖于 goroutine 启动顺序、命名或循环索引,也不受代码书写顺序影响。
✅ 正确理解要点:
- 无缓冲 channel 的发送操作是同步且阻塞的:必须有接收者就绪才能完成;
- 多个发送者就绪时,Go 不保证 FIFO 或轮询(round-robin),而是由调度器按内部策略(如 GMP 模型下的 P 本地队列、全局队列及窃取机制)决定唤醒顺序;
- 实际运行结果可能连续出现多个 "ping" 或 "pong",尤其在多核环境(GOMAXPROCS > 1)下竞争更明显。
⚠️ 常见误区澄清:
❌ “先启动的 goroutine 一定先发送” —— 错。启动顺序 ≠ 执行顺序;
❌ “for 循环计数器 i 决定优先级” —— 错。i 仅用于本地计数,与调度无关;
❌ “channel 自带公平性保障” —— 错。Go 明确不承诺 goroutine 调度公平性(参见 Go Memory Model 文档)。
? 若需实现真正的“乒乓”交替(即严格 ping → pong → ping → pong…),必须引入显式同步机制,例如使用两个 channel 构建双向握手:
func main() {
ping := make(chan struct{})
pong := make(chan struct{})
done := make(chan bool)
go func() {
for i := 0; i < 5; i++ {
<-ping
fmt.Println("ping")
pong <- struct{}{}
}
done <- true
}()
go func() {
for i := 0; i < 5; i++ {
<-pong
fmt.Println("pong")
ping <- struct{}{}
}
}()
// 启动第一次 ping
ping <- struct{}{}
<-done
}? 总结:Go channel 本身不提供发送者排序语义;任何依赖“自然交替”的设计都隐含竞态风险。生产代码中,若需确定性协作,请始终通过 channel 配对、sync 包原语(如 Mutex/Cond)或更高级的控制流(如 errgroup、context)显式建模依赖关系。










