性能与并发注意:context 不可跨 goroutine、连接池、SQL 注入预防
Go 的并发模型强大,但用错 context 与连接会导致数据竞争、资源泄漏或慢请求。本章集中讲三个最常踩的并发/性能问题,概念以官方文档(pkg.go.dev 的 context、database/sql)为准。
1. context 的生命周期:别跨 goroutine 偷用
请求的 c.Request.Context() 在 handler 返回后会被取消,把它交给活得更久的 goroutine,会立刻拿到已取消的 ctx:
func Bad(c *gin.Context) {
go func() {
// 请求结束后 ctx 已取消,这条命令直接失败
rdb.Get(c.Request.Context(), "key").Result()
}()
c.JSON(200, gin.H{"ok": true})
}
正确姿势:后台任务用独立根上下文并自己设超时:
func Good(c *gin.Context) {
userID := c.GetUint("userID") // 只把值带走,不带 ctx
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
asyncNotify(ctx, userID)
}()
c.JSON(200, gin.H{"ok": true})
}
2. 连接池:全局一个、配置合理
database/sql 自带连接池,GORM 底层就是它。常见错误是每请求新建连接或从不设上限:
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(50) // 超过就排队,别盲目调大
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour) // 定期轮换,防断线
// 客户端应全局单例,handler 里复用 *gorm.DB
连接耗尽时新请求会阻塞等待,调优方向是缩短事务持有时间,而不是无限加大池子。
3. SQL 注入预防:永远参数化
用 GORM 时坚持问号占位,永不拼接:
// 危险:name 里放 ' OR 1=1 -- 会改写整条 SQL
// db.Where("name = '" + name + "'").Find(&us)
// 安全:占位符参数化
db.Where("name = ?", name).Find(&us)
// 原生 SQL 同样用参数
db.Raw("SELECT * FROM users WHERE name = ? AND age > ?", name, age).Scan(&us)
原则:任何用户输入(查询串、路径参数、请求体)都不可信。
4. 数据竞争与排查
多 goroutine 写同一个 map/切片是典型竞争。用 go test -race ./... 或给压测加 -race 能抓出来;修复思路:加锁、用 channel 串行化,或让每 goroutine 用独立数据。
注意点
- 别在 goroutine 里直接写
gin.Context(含c.JSON),它非并发安全。 context.WithTimeout记得defer cancel(),否则定时器泄漏。- 连接池参数没有万能值:结合压测与 pprof 观察等待时间再调。
小结
三条红线:请求 ctx 不外借、连接全局复用并配池、SQL 永远占位符。配合 -race 与压测,多数并发问题能在上线前暴露,安全主题下一章继续。