性能与并发注意: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 与压测,多数并发问题能在上线前暴露,安全主题下一章继续。

笔记加载中…