不用net/http而选Gin,因其开箱即用日志/panic恢复、自动绑定校验、结构化响应,省60%样板代码且并发安全;高并发防重复/丢失需解耦接收与发送,用内存队列+worker池、traceID全链路追踪、Redis幂等控制;禁用gin.DebugMode,改ReleaseMode并配CustomRecovery和json-iterator/go优化性能。

为什么不用 net/http 直接写短信下发服务
因为短信下发接口看似简单,实则要扛住突发流量(比如营销活动瞬间 5000+ QPS),还要处理签名验签、频率限流、模板校验、异步落库、回调通知等逻辑。net/http 每加一个功能就得手动补胶水代码:自己 parse query/body、自己写中间件做鉴权、自己 handle panic、自己序列化错误响应——上线前一晚还在 debug r.ParseForm() 返回空值的问题。
而 gin.Default() 开箱就带日志 + recover 中间件,c.ShouldBindJSON() 自动校验字段和 tag,c.JSON() 一键返回结构化响应,省掉至少 60% 的样板逻辑。更重要的是,它底层复用 net/http.Server 的 goroutine 模型,天然并发安全,你只管写业务,不用操心“怎么让 1000 个请求不互相踩内存”。
如何避免高并发下短信重复下发或丢失
核心矛盾在于:HTTP 请求是瞬时的,但短信网关调用有延迟、可能失败,而用户点击“发送”后刷新页面会重试——必须把“接收请求”和“真正发短信”解耦。
- 别在 handler 里直接调第三方短信 SDK:
sendSms(req)这种同步写法会卡住 goroutine,QPS 上不去,还容易因超时导致连接堆积 - 改用内存队列 + worker 池:用
chan *SmsTask接收请求,后台起 4–8 个 goroutine 消费;任务结构体里带上原始c.Request.Context(),方便超时控制 - 每条任务生成唯一 traceID,从收到请求到回调结果全程透传,出问题能快速定位是卡在验签、限流还是网关返回 503
- 对重复请求做幂等:前端带
X-Request-ID,后端用 Redis SETNX 存 2 分钟,已存在就直接返回 success,不走后续流程
gin.Context 在长耗时任务中容易被提前释放
常见错误是把 *gin.Context 保存到全局 map 或传给 goroutine 异步用,结果 handler 函数一返回,c.Request 和 c.Writer 就失效了,异步写响应会 panic 或静默丢数据。
立即学习“go语言免费学习笔记(深入)”;
正确做法只有两个:
- 如果必须异步返回结果(比如轮询状态),改用 WebSocket 或 SSE,用
conn.SetWriteDeadline()防 goroutine 卡死 - 如果只是记录结果,那就只取需要的字段:
reqID := c.GetHeader("X-Request-ID")、userID, _ := c.Get("user_id"),然后把它们塞进任务结构体,彻底和*gin.Context解绑
千万别写 go func() { c.JSON(200, resp) }() —— 这是 Gin 项目上线前最常被 pprof 抓出来的“幽灵 goroutine”来源。
上线前必须关掉的 gin.DebugMode
gin.DebugMode 在开发时很友好:自动 reload 模板、渲染彩色 panic 页面、记录完整栈信息。但生产环境开着它,每个请求都会多做三件事:检查文件变更、构造 HTML 错误页、缓存 error 字段的完整栈——实测会让 P99 延迟升高 15–20ms,QPS 下降 12%。
切到 gin.ReleaseMode 后,记得补两件事:
- 手动注册
gin.CustomRecovery,否则 panic 会直接 crash 进程,而不是返回 500 - 所有 JSON 序列化统一走
encoding/json替换为json-iterator/go,基准测试显示小 payload 场景下吞吐量能从 8500 提升到 15200 req/s
真正的瓶颈从来不在框架选型,而在于你有没有在第一个 handler 里就做了 time.Sleep(100 * time.Millisecond) —— 先跑 pprof,再改代码。


















