
本文解释为何多个 goroutine 可同时向同一无缓冲 channel 发送数据而不阻塞——关键在于有另一个 goroutine 持续接收,使发送操作能交替完成,符合 go 通道的同步通信机制。
在 Go 中,无缓冲 channel(unbuffered channel)本质上是一个同步通信原语:每次 ch 配对完成,且整个过程是原子的、无中间状态的。这与“队列积压”或“写入次数限制”无关——它不计数,也不缓存;它只协调协作。
回到你的代码:
func main() {
ch := make(chan string) // 无缓冲 channel
go write(ch)
go write2(ch)
go read(ch)
select {} // 阻塞主 goroutine,防止程序退出
}
func write(ch chan string) {
for {
ch <- "write1" // 阻塞,直到有人接收
}
}
func write2(ch chan string) {
for {
ch <- "write2" // 同样阻塞,等待接收
}
}
func read(ch chan string) {
for {
time.Sleep(time.Second)
select {
case res := <-ch:
fmt.Println("received:", res)
case <-time.After(200 * time.Millisecond):
fmt.Println("timeout")
}
}
}这段代码能持续运行,并非因为“允许多次写入”,而是因为 read goroutine 在循环中周期性尝试接收(每秒一次,配合 200ms 超时兜底),每当它成功执行
⚠️ 注意以下关键点:
- 没有“写入配额”:无缓冲 channel 不限制总发送次数,只限制“未完成的发送”数量——始终为 0 或 1(因为未配对的发送必然阻塞)。
- goroutine 调度不可预测:write 和 write2 哪个先被唤醒取决于调度器,因此输出顺序是不确定的(如 "write1"、"write2"、"write1" 交错出现),这是并发的正常表现。
- 超时逻辑的作用:read 中的 time.After 并非用于“避免阻塞”,而是为了防止在无数据可读时永久挂起(例如所有写 goroutine 暂停)。但只要至少一个写 goroutine 处于活跃发送状态,
- 内存模型保障:根据 Go Memory Model,channel 的发送与接收操作构成同步事件(synchronization event),确保跨 goroutine 的变量读写可见性。无需额外加锁。
✅ 正确实践建议:
- 若需严格顺序(如先 write1 后 write2),应引入显式同步(如 sync.Mutex 或通过 channel 编排流程);
- 若写入频率远高于读取能力,考虑使用带缓冲 channel(make(chan string, N))缓解背压,但需注意缓冲区满时仍会阻塞;
- 生产环境中,务必为所有 goroutine 设定退出机制(如通过 context.Context 或 done channel),避免泄漏。
总之,多 goroutine 写同一无缓冲 channel 是完全合法且常见的模式,其可行性不源于“通道宽容”,而源于 Go 并发模型的核心设计:通信即同步(Do not communicate by sharing memory; share memory by communicating)。










