http handler中不应直接go run业务逻辑,而应校验签名、读取解析body、同步写入持久化队列后立即返回200;redis stream配合消费者组实现可靠异步任务,需正确使用xreadgroup、xack、xclaim及setnx防重;go中优先用channel而非回调函数处理异步结果。

HTTP handler 里不能直接 go run 业务逻辑
很多开发者一看到“异步”,就在 http.HandlerFunc 里写 go doSomething(),以为这就完成了异步。实际这是危险的速食方案:goroutine 可能访问已释放的请求上下文(如 r.Body 已被关闭)、无法控制并发数、panic 会静默丢失、进程重启时所有待执行任务彻底消失。
真正该做的是:在 handler 中只做三件事——校验签名、读取并解析原始 body、将结构化任务数据同步写入持久化队列(如 Redis Stream 或 PostgreSQL 表)。之后立刻返回 200 OK,不写任何响应体。
- 必须用
io.ReadAll(r.Body)一次性读完,否则后续读取会失败或阻塞 - 签名校验(如
X-Hub-Signature-256)要在读 body 后立即用hmac.Equal做常数时间比对,防止时序攻击 - 任务入队必须检查返回值,例如
redis.Client.XAdd返回错误时,应记录日志并返回400 Bad Request
用 Redis Stream 实现带 ACK 和重试的消费者
Redis Stream 是轻量级生产环境异步任务的事实标准,它天然支持消费者组、消息确认、超时重分配和死信归档,不需要额外部署 Kafka 或 RabbitMQ。
关键点不在“怎么起 goroutine”,而在于“怎么保证每条消息至少处理一次”。消费者启动时需先确保组存在:redis.XGROUP CREATE order_events orders $ MKSTREAM;拉取消息必须用 XREADGROUP GROUP orders worker-01 COUNT 10 BLOCK 5000 STREAMS order_events >,其中 > 表示只读新消息,避免重复消费历史积压。
- 业务逻辑执行成功后,再调用
XACK;提前 ACK = 消息丢失 - 若处理失败(如第三方 API 超时),不要
XACK,让 Redis 在TIMEOUT(默认 60s)后通过XCLAIM重新分配给其他 worker - 为防幂等问题,建议在业务逻辑开头用
SETNX task_id_ttl 3600做去重锁,失败则直接 return
回调函数在 Go 里不是首选通信方式
Go 语言中显式传入 func(result string, err error) 类型的回调函数,只适合极简场景(如单元测试模拟、命令行工具内部链式调用)。它会让错误传播不可控、调试困难、无法跨 goroutine 安全传递上下文(比如 context.Context)。
更符合 Go 风格的做法是:用 channel + struct 封装结果,配合 select 处理超时与取消。例如发起一个支付回调通知,你真正需要的不是“等它回调我”,而是“发出去,然后等它完成或超时”。
- 不要写
doPayment(req, func(resp *Resp, err error) { ... }) - 应该写
respCh := make(chan *Resp, 1); go doPayment(req, respCh); select { case r := - 如果回调来自外部系统(如微信支付回调),那它本身就是 HTTP 请求,你的服务只需作为 Webhook 接收端,按前两节方式处理即可
goroutine 泄漏和 context 超时是高频翻车点
很多人用 sync.WaitGroup 管理一批 goroutine,却忘记加 defer wg.Done(),或者把 wg.Wait() 放在 http.ResponseWriter 已写出状态码之后,导致连接 hang 住;更多人忽略 context.WithTimeout 的生命周期管理,让后台 goroutine 持有已 cancel 的 context,继续往数据库写无效日志。
可靠做法是:所有对外部服务的调用(HTTP、DB、Redis)都必须接收 ctx context.Context 参数,并在调用前用 ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second) 包裹;cancel 必须在 goroutine 结束时调用,且不能依赖 defer —— 因为 defer 在函数返回时才执行,而 goroutine 可能早于主函数结束。
- 错误示范:
go apiCall(ctx),其中ctx来自主 handler,可能已在 response 写出后被 cancel - 正确写法:
go func(ctx context.Context) { ... }(context.WithValue(parentCtx, key, value)) - 对 DB 查询,优先用
db.QueryContext(ctx, ...)而非db.Query(...),否则超时不会中断查询
真正的难点从来不在“怎么启 goroutine”,而在于“怎么让它安全退出、可观测、可追溯、可重试”。别把 go 当开关,它只是执行载体;持久化队列才是异步任务的底盘,context 才是它的油门和刹车。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











