缓冲区满时 goroutine 永久阻塞,因发送操作必须等待接收者腾出空间才能继续;Go 调度器不切换该 goroutine,直至条件满足,这是同步阻塞而非错误或超时。

为什么缓冲区满会导致 goroutine 永久阻塞
向已满的 chan T 发送数据会直接阻塞当前 goroutine,直到有接收者腾出空间。这不是“慢”,而是彻底停住——调度器不会切换它,它就卡在 ch 这一行,无法继续执行后续逻辑,也无法被回收。
- 无缓冲 channel:只要没接收方,
ch 立即阻塞(本质是同步等待) - 有缓冲 channel:仅当
len(ch) == cap(ch)时才阻塞;否则写入缓冲区后立即返回 - 阻塞不等于错误,但若接收方异常退出、漏读或根本不存在,就会演变成死锁
select + default 是最常用的非阻塞写法
别用 if ch (语法错误),要用 <code>select 配合 default 分支来试探是否可写:
select {
case ch <- v:
// 成功写入
default:
// 缓冲区满,跳过或降级处理(如打 warn 日志)
}-
default必须存在,否则select会退化为阻塞发送 - 不要在
default里加time.Sleep或重试循环——这掩盖了背压问题,不是缓解,是拖延 - 适合允许丢数据的场景:监控上报、心跳、日志采样等
超时控制比增大缓冲区更可控
用 time.After 给发送加时限,避免无限等待:
select {
case ch <- v:
// 成功
case <-time.After(100 * time.Millisecond):
// 超时,放弃本次发送
}- 超时时间要结合业务容忍度设,比如用户请求响应通常 ≤ 500ms,后台任务可放宽到几秒
- 单纯调大缓冲区(比如从 64 改成 1024)吞吐提升微乎其微,但内存占用和 GC 压力明显上升
- 真正瓶颈往往不在 buffer 大小,而在消费者处理速度——先优化
<-ch后的逻辑,再考虑 buffer
关闭 channel 不解决“满”的问题,但影响接收行为
关闭一个已满的带缓冲 channel 是合法的:close(ch) 只设置 closed 标志,不影响缓冲区中已存在的值。接收方仍能读完所有剩余数据,之后再读才返回零值+false。
立即学习“go语言免费学习笔记(深入)”;
- 向已关闭的
ch发送会 panic:panic: send on closed channel - 接收方无法通过
len(ch)或其他方式感知“满”,只能靠ok判断是否 closed - 别指望 close 来“清空”缓冲区——它只是通知“不再有新数据”,已有数据照常排队
缓冲区溢出本身不触发 panic,但它是系统背压的明确信号。重点不是让它不溢出,而是让程序对溢出有明确响应路径:丢弃、降级、限流或告警。硬扛靠堆 buffer,迟早 OOM 或延迟失控。


















