直接在goroutine中使用未复制的*gin.Context会panic,必须调用c.Copy()获取只读快照,仅可读取URL、Header等字段,不可调用JSON/String/Abort等写操作。

gin.Context 在 goroutine 中直接使用会 panic
直接在新 goroutine 里用原始 c(即未复制的 *gin.Context)读写请求或响应,运行时大概率触发 panic: write on closed body 或 context canceled。因为 Gin 在 handler 返回后立即回收上下文资源,而 goroutine 可能还在执行。
必须显式调用 c.Copy() 创建一个独立副本,该副本持有请求快照(如 URL、Header、Query),但不包含响应体写入能力——这点常被忽略。
-
c.Copy()复制的是只读快照,不能调用c.JSON()、c.String()等写响应方法 - 若需异步写日志或发消息,应只取
c.Request.URL.Path、c.ClientIP()、c.Get("user_id")这类只读字段 - 不要试图在 goroutine 中调用
c.Abort()或修改c.Writer,它已失效
多个 goroutine 共享 channel 时需注意缓冲与关闭时机
用 chan string 或 chan *gin.Context 做任务分发时,常见错误是 channel 无缓冲且未及时消费,导致主 handler 卡在发送端(如 ch 阻塞),表面看像“并发变串行”。
推荐做法:明确 channel 类型和生命周期,避免跨 goroutine 误关。
- 使用带缓冲的 channel(如
make(chan string, 10)),防止生产者阻塞 - 由创建 channel 的 goroutine 负责
close(),其他 goroutine 只读不关 - 若用
range ch消费,务必确保 channel 会被关闭,否则永远阻塞 - 不要把
context.Context(如c.Request.Context())直接塞进 channel 传给后台 goroutine——它的取消信号可能提前终止异步任务
并发写共享 map 或全局变量必须加锁
Gin handler 默认并发执行,若多个请求同时写同一个 map[string]int 或全局计数器,会出现 fatal error: concurrent map writes。
这不是 Gin 的问题,而是 Go 运行时对 map 的并发写保护机制触发的崩溃。
- 用
sync.Map替代原生map,适合读多写少场景 - 高频读写且需精确控制时,用
sync.RWMutex包裹普通 map - 避免在 handler 中直接操作未加锁的全局变量(如
var totalRequests int),改用atomic.AddInt64(&totalRequests, 1) - 数据库连接池(
*sql.DB)本身线程安全,但必须提前调用db.SetMaxOpenConns()和db.SetConnMaxLifetime(),否则连接争抢会拖慢整体吞吐
HTTP/1.1 keep-alive 会让日志时间戳看起来串行
浏览器连续发两个 GET /api 请求,服务端日志显示第二条比第一条晚 2 秒,容易误判为“Gin 串行处理”。实际是客户端复用 TCP 连接,第二个请求在第一个响应返回后才发出。
验证方法:用 curl -H "Connection: close" 强制禁用 keep-alive,或用 ab、hey 并发压测,观察真实并发行为。
- 服务端无法控制客户端是否复用连接,所以日志时间戳不能作为并发判断依据
- 真正要看的是每个 handler 内部耗时(用
time.Since(start)记录),而非日志打印顺序 - 若所有 handler 耗时都稳定在 10ms,但日志间隔 500ms,基本可断定是客户端行为或终端输出缓冲导致
真正难处理的不是“怎么开 goroutine”,而是“哪些数据能安全跨 goroutine 流动”。Context 快照、channel 缓冲策略、共享状态同步——这三处出错,不会立刻报错,但会在高并发下随机崩。











