channel阻塞导致卡住是go的正常语义,非bug;常见死锁因所有goroutine都在等待channel操作。需区分使用场景:任务分发用带缓冲channel,信号通知用chan struct{},管道处理须确保读写goroutine均启动。

channel阻塞时程序卡住,怎么判断是设计问题还是误用?
Go里channel默认是无缓冲的,往里面send却没人recv,或者反过来,协程就直接挂起。这不是bug,是语义——但多数时候你其实没打算让主goroutine等在那里。
常见错误现象:fatal error: all goroutines are asleep - deadlock
这说明所有goroutine都在等channel,没人推进流程。
使用场景要分清:
- 任务分发/结果收集:用带缓冲的
channel(比如make(chan int, 10))能缓解阻塞,但不解决根本逻辑问题 - 信号通知(如退出、暂停):优先用
chan struct{},零内存开销,语义也更清晰 - 管道式处理(a → b → c):必须确保每个环节都启动了goroutine,且至少有一个goroutine在读、一个在写
容易踩的坑:
- 忘记启动接收方,只写了
ch 就结束main函数 - 在循环里反复创建新
channel却不关闭,导致goroutine泄漏 - 把
channel当共享变量在多个goroutine里随意读写,没加同步控制
实操建议:加个select配default分支做非阻塞试探;或用len(ch)和cap(ch)查当前状态(仅限带缓冲channel)
立即学习“go语言免费学习笔记(深入)”;
close(channel)之后还能读吗?哪些情况会panic?
能读,但行为要看怎么读。
关闭后的channel仍可安全recv,直到所有已发送值被取完;之后再读,会得到零值+false(第二个返回值)。
容易踩的坑:
- 对已关闭的
channel继续send→panic: send on closed channel - 多个goroutine同时
close同一个channel→ 同样panic - 在
range遍历时,close后循环自动退出,但若中途有其他goroutine还在往里send,可能漏数据
使用场景提示:
- 关闭动作应由“发送方”负责,这是约定,不是强制语法
- 如果不确定谁该关,改用
sync.WaitGroup配合done chan struct{}更稳妥 - 不要用
close(nil),会直接panic
实操建议:关闭前加一层检查:if ch != nil { close(ch) };读取时统一用v, ok := 判断是否到尾
buffered channel和unbuffered channel性能差多少?
差别不在吞吐量,而在调度开销和内存占用。
unbuffered channel每次send/recv都触发goroutine切换:发送方挂起,直到接收方就绪,反之亦然。这带来上下文切换成本,但换来强同步语义(类似“握手”)。
buffered channel把值暂存在内存队列里,只要没满/空,就不会阻塞goroutine。所以:
- 小缓冲(如
make(chan int, 1))适合解耦生产消费节奏,避免频繁切换 - 大缓冲(如
make(chan []byte, 1000))可能吃掉大量内存,且掩盖背压问题 - 缓冲大小为0等价于unbuffered,别写
make(chan int, 0),直接写make(chan int)更清晰
性能影响真实案例:
- 高频小消息(如事件通知):unbuffered更轻量,因为不分配额外内存
- 批量数据传递(如图像帧):buffered能摊平goroutine唤醒成本,但要注意
cap别设成math.MaxInt
实操建议:先用unbuffered验证逻辑;压测发现goroutine频繁阻塞,再按实际吞吐加缓冲;别盲目调大cap,先看len(ch)是否长期接近cap(ch)
select + channel组合里default分支到底要不要加?
要看你是否允许“这次操作跳过”。
没有default时,select会阻塞,直到某个case就绪;加了default,它就变成非阻塞尝试——哪怕所有channel都不可读/不可写,也会立刻执行default分支。
常见错误现象:
- 在for循环里写
select { default: time.Sleep(1 * time.Millisecond) }→ CPU空转,100%占用 - 用
default代替超时控制,结果逻辑被意外跳过(比如本该等信号,却因default立即往下走)
使用场景差异:
- 消息轮询(如心跳检测):需要
default+time.Sleep,但sleep时间不能太短 - 资源争抢(如抢锁):
default可用来实现“试获取”,失败就干别的 - 主循环协调多个channel:通常不加
default,靠time.After或context.WithTimeout控超时
实操建议:加default前先问自己:“如果所有channel此刻都不就绪,我是否真的希望立刻执行某段逻辑?”;大多数业务协调场景,应该用case 替代<code>default
channel不是队列,也不是锁,它本质是goroutine之间的通信契约。很多人卡住,不是不会写ch ,而是没想清楚“谁发、谁收、什么时候关、失败了谁兜底”。这些细节藏在业务流里,没法靠语法糖绕过去。










