chan []t 比 chan t 更适合批处理,因其以批次为原子单位发送,天然表达“一批任务到达”语义,避免接收端自行攒批、计时、判边界导致的漏触发或延迟。

为什么 chan []T 比 chan T 更适合批处理
因为单个 chan T 无法天然表达“一批任务到达”的语义,接收端得自己攒、计时、判断边界——容易漏触发或延迟。而 chan []T 把批次作为原子单位发送,逻辑更清晰,也避免了在 goroutine 里反复 select + 计时器的复杂状态管理。
常见错误现象:for range ch { /* 处理单个 T */ } 看似简洁,但实际业务中往往需要等满 100 条才发请求,硬攒会阻塞发送方,用 time.After 补救又容易丢数据或重复提交。
- 使用场景:向外部 API 批量写入日志、同步数据库记录、上报监控指标
- 参数差异:
chan []String要求发送方明确构造切片;接收方拿到的就是完整批次,无需再聚合 - 性能影响:减少 channel 通信次数(100 次单条 → 1 次百条),降低调度开销;但要注意切片底层数组共享风险,必要时用
append([]T(nil), batch...)做浅拷贝
select + default 非阻塞收包的坑
想“有就收,没就干别的”,很多人直接写 select { case b := 。问题在于:如果 <code>batchCh 是无缓冲 channel,且发送方还没发,default 会立刻执行,导致忙轮询;如果是带缓冲的,又可能因缓冲区未满而跳过本该收的批次。
- 正确做法:只在明确需要“快速响应其他任务”时用
default,且搭配time.Sleep或runtime.gosched()控制频率 - 更稳妥的替代:用
time.After设超时,比如case b := - 兼容性注意:Go 1.21+ 对短超时(time.After 仍可能被调度器忽略,别依赖精确微秒级响应
如何安全关闭 chan []T 并确保最后一包不丢
关闭 channel 本身很简单,但接收方若用 for b := range batchCh,会在关闭后自动退出,而发送方可能刚把最后一包 append 到切片、还没来得及 send —— 这包就永远卡在 goroutine 本地变量里,丢了。
立即学习“go语言免费学习笔记(深入)”;
- 必须让发送方负责“通知结束”:先发完所有批次,再单独发一个哨兵值(如
nil或空切片),接收方遇到就 break - 示例:
batchCh ,接收端 <code>if b == nil { break };别用close(batchCh)代替 - 性能提示:哨兵值用
nil比空切片更轻量(零分配),且nil != []T{},类型安全可判
批量大小设多少?别硬编码 100
写死 const batchSize = 100 看似省事,但不同场景下最优值差很远:小对象(如 []byte{1,2,3})可设到 1000,大结构体(含嵌套 map)可能 10 就触发 GC 压力。而且网络层 MTU、下游 API 限流、内存碎片都会影响实际吞吐。
- 建议从配置读:
os.Getenv("BATCH_SIZE")或命令行 flag,上线后可动态调 - 观察指标:看 goroutine 堆栈里是否频繁出现
runtime.gopark(说明 channel 常阻塞),或runtime.MemStats.Alloc是否突增(说明切片分配太猛) - 容易被忽略的点:如果用
make([]T, 0, N)预分配,N和最终len不一致时,底层数组不会缩容——长期运行下,内存只涨不跌