Go Channel缓冲区为何未按预期限制读写?原因探究
我尝试用Go Channel实现两个协程间的通信,最初创建了无缓冲的int类型Channel传给Worker协程,让它输出0到9的数字序列,但程序输出不符合预期。
无缓冲Channel核心代码
func Worker(identifier string, ch chan<- int) { fmt.Printf("Entering worker %s\n", identifier) for i := 0; i < 10; i++ { fmt.Printf("Writing %d\n", i) ch <- i } fmt.Printf("Exiting worker %s\n", identifier) close(ch) } func main() { ch := make(chan int) go Worker("1", ch) for v := range ch { fmt.Printf("Reading %d\n", v) } }
执行输出
Entering worker 1 Writing 0 Writing 1 Reading 0 Reading 1 Writing 2 Writing 3 Reading 2 Reading 3 Writing 4 Writing 5 Reading 4 Reading 5 Writing 6 Writing 7 Reading 6 Reading 7 Writing 8 Writing 9 Reading 8 Reading 9 Exiting worker 1
可以看到存在两次写入操作完成后再执行两次读取的情况。
缓冲大小设为3后的代码
修改后的main函数:
func main() { ch := make(chan int, 3) // <= Buffer go Worker("1", ch) for v := range ch { fmt.Printf("Reading %d\n", v) } }
执行输出
Entering worker 1 Writing 0 Writing 1 Writing 2 Writing 3 Writing 4 Reading 0 Reading 1 Reading 2 Reading 3 Reading 4 Writing 5 Writing 6 Writing 7 Writing 8 Writing 9 Reading 5 Reading 6 Reading 7 Reading 8 Reading 9 Exiting worker 1
现在是5次写入完成后再执行5次读取的情况。
我的疑问:为何会出现这样的执行行为?无缓冲Channel不是应该每次仅完成一次写入和一次读取吗?另外,缓冲大小设为3时,为何是5次读写一批而非3次?
解答
关于无缓冲Channel的行为
首先明确:无缓冲Channel的ch <- i操作确实会阻塞,直到有其他协程执行<-ch接收数据,你看到的"两次写入后两次读取"是标准输出(stdout)的缓冲机制带来的错觉,并非Channel的实际传输顺序。
Go的fmt.Printf输出并非直接实时显示到终端,而是先写入用户态缓冲区,当缓冲区满、遇到换行符或程序触发刷新时,才会将内容输出到终端。比如Worker协程执行完fmt.Printf("Writing 0\n")后,内容先存在缓冲区,还没来得及显示,代码继续执行ch <- 0阻塞;main协程接收数据后执行fmt.Printf("Reading 0\n"),此时可能触发缓冲区刷新,导致之前的Writing 0、Writing 1(Worker协程在被唤醒后又执行了一次打印)和Reading 0、Reading 1一起输出,看起来像是批量写后批量读,但实际Channel的传输是严格的"写阻塞→读唤醒→读完成→写继续"的一对一流程。
关于缓冲大小为3时的行为
同样核心原因是标准输出缓冲。缓冲Channel的ch <- i仅在缓冲区满时阻塞:当缓冲大小为3时,Worker可以连续写入0、1、2(缓冲区满),此时再写入3会阻塞,直到main协程读取一个元素空出缓冲区位置。但你看到连续5次写入的输出,是因为Worker的多次fmt.Printf输出被stdout缓冲暂时存储,直到main协程开始输出读取内容时,才触发缓冲区一次性刷新,把攒下的5条写入日志全部显示出来,之后又重复这个过程,呈现出"5次写→5次读"的视觉效果,和Channel的实际缓冲大小3无关。
总结:
- 无缓冲Channel的实际传输是严格一对一的阻塞唤醒流程,批量输出是stdout缓冲导致的视觉偏差。
- 缓冲大小为3时,所谓的"5次批量读写"同样是stdout缓冲延迟刷新的结果,Channel本身仍按照缓冲区状态正常阻塞和继续。
内容的提问来源于stack exchange,提问作者João Victor Dantas

