直接在 handler 里开 goroutine 会 panic,因为 gin.Context 是 request-scoped 的,其底层 *http.Request 和 http.ResponseWriter 在 handler 返回后被回收或复用;goroutine 中调用 c.Param()、c.PostForm() 或 c.JSON() 将导致 panic 或读取脏数据。

为什么直接在 handler 里开 goroutine 会 panic?
因为 gin.Context 是 request-scoped 的,底层绑定的 *http.Request 和 http.ResponseWriter 在 handler 函数返回后会被 net/http 回收或复用。如果你在 goroutine 里调用 c.Param()、c.PostForm() 或 c.JSON(),大概率触发 panic 或读到空/脏数据。
如何安全地并发执行多个独立任务并合并结果?
核心是:用 sync.WaitGroup 等待所有 goroutine 完成,用 channel 收集结果,且所有耗时操作必须在 handler 返回前启动、不能依赖 c。
- 每个 goroutine 只接收从
c提前提取的参数(如userID、payload),不捕获*gin.Context - 用
make(chan string, 1)这类带缓冲的 channel 避免 goroutine 永久阻塞 - 必须显式调用
wg.Done(),否则wg.Wait()永不返回 - 不要在 goroutine 里写响应 —— 响应只由主 goroutine 调用
c.JSON()一次完成
func(c *gin.Context) {
userID := c.GetString("user_id")
var wg sync.WaitGroup
resCh1 := make(chan string, 1)
resCh2 := make(chan string, 1)
<pre class="brush:php;toolbar:false;">wg.Add(1)
go func() {
defer wg.Done()
resCh1 <p>}</p>并发合并时容易忽略的 timeout 和 error 处理
并发本身不解决超时问题 —— 如果某个子任务卡住,整个 handler 会无限等待。必须为每个 goroutine 设置上下文 deadline,并用 select 控制 channel 读取。
- 用
context.WithTimeout包裹每个子任务,避免单点故障拖垮整体 - channel 读取必须配超时分支,否则
可能永久阻塞 - 错误不应被吞掉:把 error 也通过 channel 发出,统一判断是否需降级返回
- 注意
time.Sleep不等于真实 I/O 耗时,压测时务必用真实 HTTP 或 DB 调用验证
什么时候不该用并发合并?
当任务之间有强依赖(比如第二个请求需要第一个的结果)、或共享状态(如共用一个未加锁的 map)、或总耗时远小于网络 RTT 时,并发反而增加调度开销和出错概率。
- 简单数据库查询 + 模板渲染,通常串行更快 —— Go 调度器开销不是零
- 若两个 API 都调同一第三方服务,还可能触发对方限流,导致雪崩
- 调试困难:goroutine 中 panic 不会自动被捕获进 Gin 的 Recovery 中间件
真正要小心的不是“能不能并发”,而是“并发后失败怎么兜底”。没做超时、没设重试、没留日志的并发,只是把问题从显性延迟变成了隐性丢数据。











