应按场景选异步模式:瞬时操作用带recover的goroutine;可控队列用带缓冲channel+固定worker;跨服务必须用Kafka等持久化MQ,避免channel死锁与泄漏。
用 goroutine + channel 实现异步消息处理,不是“能跑就行”,而是要分清场景:是本地轻量任务、高并发写入,还是需要持久化与重试的业务消息。选错模式,轻则丢数据,重则 goroutine 泄漏或 channel 死锁。
goroutine 就够了?适合不关心结果、无状态、失败可容忍的瞬时操作,比如日志上报、埋点记录、通知触发。
go sendEmail(email) —— 若 sendEmail panic,整个 goroutine 崩溃且无日志recover + 显式传参(避免闭包捕获循环变量)func sendEmailHandler(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("to")
go func(to string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in email goroutine: %v", r)
}
}()
if err := sendEmailAsync(to); err != nil {
log.Printf("send email to %s failed: %v", to, err)
}
}(email)
json.NewEncoder(w).Encode(map[string]string{"status": "queued"})
}channel 构建可控的任务队列?当任务量不可控、需限流或统一管理生命周期时,必须引入带缓冲的 channel 作为中间队列,再配固定数量 worker 消费。
taskCh := make(chan Task, 100):缓冲大小 ≠ 并发数,而是积压容量,防主流程阻塞runtime.NumCPU(),先压测close(taskCh),否则 for range taskCh 永远不会退出
type Task struct {
ID int
Data string
}
func StartWorkerPool(numWorkers int, taskCh <-chan Task) {
for i := 0; i < numWorkers; i++ {
go func(workerID int) {
defer func() {
if r := recover(); r != nil {
log.Printf("worker %d panicked: %v", workerID, r)
}
}()
for task := range taskCh {
log.Printf("Worker %d processing task %d", workerID, task.ID)
// do work...
}
}(i + 1)
}
}
channel 做跨服务异步通信?channel 是内存级通信,进程重启即丢失,无法满足消息可靠性要求(如订单创建后发短信、支付成功后更新库存)。它只适用于单体或同一进程内协作。
github.com/segmentio/kafka-go)消费时,每条消息处理完才调用 msg.Commit(),否则可能重复消费json.Marshal,反序列化前校验字段,避免 panictrace_id 和 timestamp,否则出问题时根本无法对账和排查时序90% 的 goroutine 相关线上故障,都来自 channel 使用不当。以下现象出现就该立刻检查:
chan send 或 chan receive —— 很可能是 sender 没 close,receiver 却用了 for range
runtime.ReadMemStats 显示 Mallocs 高但 Frees 低 —— 可能是 channel 缓冲区过大,或结果 channel 没被读取导致堆积taskCh 处阻塞等待
select 等待 channel 时没写 default 或 time.After 分支 —— 一旦 channel 没数据,协程就卡死真正难的不是写出能跑的异步代码,而是判断哪一层该用内存 channel,哪一层必须交出去给消息队列;以及当 panic 发生时,是否真的被 recover 住、错误是否被记录、任务是否真的被丢弃而非静默失败。这些细节,往往在压测和上线后第一波流量里才暴露出来。