无缓冲 channel 易致 goroutine 频繁阻塞,应改用带缓冲 channel;缓冲区大小需权衡内存与延迟,典型值如 make(chan *LogEntry, 1024)。
无缓冲 channel(make(chan int))在每次发送和接收时都需双方 goroutine 同时就绪,实际压测中容易成为性能瓶颈。尤其在生产者快、消费者慢的场景下,send 会直接挂起 goroutine,触发调度开销。
改用带缓冲 channel 可解耦收发节奏,但缓冲区不是越大越好——过大会增加内存占用,还可能掩盖消费延迟问题。
make(chan *LogEntry, 1024)
make(chan int, 1 这类“保险式”大缓冲,它会让 channel 占用数 MB 内存且延迟不可控
select 配合 default 实现非阻塞写入,再降级到本地队列或丢弃在循环内反复调用 make(chan ...) 或对已关闭 channel 执行 close(),会快速生成大量短期对象,加剧垃圾回收频率。pprof 中常表现为 runtime.makeslice 和 runtime.chansend 占比异常高。
关键原则:channel 是长生命周期通信载体,不是一次性的消息容器。
ch := make(chan int); go worker(ch) —— 应提前创建好 channel 并复用for v := range ch 会一直阻塞等

goroutine 数量持续上涨即可定位。
必须确保 range 的 channel 有确定的关闭时机,且关闭行为可被 receiver 感知。
close(ch),receiver 才能安全退出sync.WaitGroup 等待全部完成再 close,不能靠计数器——竞态风险高context.Context + select 替代纯 range,例如:for {
select {
case v, ok := <-ch:
if !ok {
return
}
process(v)
case <-ctx.Done():
return
}
}当 select 中所有 channel 都不可读/写时,若无 default,goroutine 会阻塞,这在需要响应超时、心跳或控制信号的场景下极危险。
更隐蔽的问题是:即使写了 default,若其中逻辑耗时过长(如打印日志、调用 HTTP),也会拖慢主循环吞吐。
time.Sleep),改用 time.After channelselect 嵌套,易引发调度延迟;可考虑用 chan struct{} 统一通知事件