直接用 go func() 启动订单处理会导致并发不安全,因 goroutine 泛滥和缺乏协调引发连接池耗尽、库存错乱等问题;应改用带缓冲 channel 作任务队列,配合固定数量 worker 并监听 ctx.Done() 实现可控并发与优雅退出。

为什么直接用 go func() 启动订单处理会出问题
并发不等于安全。很多开发者一上来就对每个订单起一个 go func(),结果发现数据库连接被打爆、库存扣减错乱、日志混成一团。根本原因在于:没有节制的 goroutine 泛滥 + 缺乏任务协调机制。比如 1000 个订单进来,瞬间 spawn 1000 个 goroutine,而数据库连接池可能只有 20 个,panic: dial tcp: lookup xxx: no such host 或 too many connections 就是典型表现。
真正需要的是可控的并发度 + 明确的任务生命周期管理 —— 这正是 channel 的核心价值。
用 chan *Order 做任务队列 + 固定 worker 数量
把订单当消息塞进 channel,由一组固定数量的 worker 从 channel 中取任务执行。这样既限流又解耦,还能自然支持优雅退出。
-
workerCount通常设为 CPU 核心数 × 1.5 ~ 2(I/O 密集型可更高),避免过度抢占调度器 - 输入 channel 必须是带缓冲的(如
make(chan *Order, 100)),否则生产者可能被阻塞,导致上游 HTTP handler 卡住 - 每个 worker 应监听
ctx.Done(),收到取消信号时完成当前任务后退出
func startWorkers(ctx context.Context, tasks chan *Order, workerCount int) {
var wg sync.WaitGroup
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case order, ok := <-tasks:
if !ok {
return
}
processOrder(order) // 实际业务逻辑
case <-ctx.Done():
return
}
}
}()
}
go func() {
<-ctx.Done()
close(tasks)
wg.Wait()
}()
}
select 配合 default 实现非阻塞投递与背压控制
上游(比如 HTTP handler)往 tasks channel 发送订单时,不能无条件 tasks ,否则在 channel 满时会阻塞整个请求处理流程。需要用 select + default 做快速失败或降级。
立即学习“go语言免费学习笔记(深入)”;
- 投递失败时可返回
429 Too Many Requests,而不是让请求 hang 住 - 也可改走本地内存队列(如
sync.Map)暂存,再异步批量刷入 channel,但会增加复杂度 - 注意:不要在
default分支里加time.Sleep循环重试 —— 这等于自己实现低效轮询,且容易拖垮 handler
func handleOrder(w http.ResponseWriter, r *http.Request) {
order := parseOrder(r)
select {
case tasks <- order:
json.NewEncoder(w).Encode(map[string]string{"status": "accepted"})
default:
http.Error(w, "system busy", http.StatusTooManyRequests)
}
}如何安全关闭 channel 并等待所有任务完成
直接 close(tasks) 是危险的 —— 如果有 worker 正在读取,会收到零值;如果多个 goroutine 同时 close 同一个 channel,会 panic:panic: close of closed channel。必须确保仅由单一 goroutine 关闭,且关闭前所有生产者已停止写入。
- 推荐用
context.WithCancel控制生命周期,关闭信号由上层统一触发 - worker 内部用
for order := range tasks自动退出,比手动select更简洁可靠 - 用
sync.WaitGroup等待所有 worker 退出后再返回,否则进程可能提前结束,导致部分订单丢失
最易忽略的一点:如果你在 worker 里调用了外部服务(如支付回调、消息推送),这些操作本身可能超时或失败,必须单独设置超时和重试策略 —— channel 只管调度,不负责业务容错。










