bun 不是 gorm,gin 整合 bun 的关键是绕过 gin 中间件对 context.context 的覆盖、显式管理 *bun.db 生命周期、避免用 c.request.context() 传 db 实例;因其超时取消语义易致查询中断,须用 context.withtimeout 包装;scan() 不匹配时不报错,需检查 rowsaffected();结构体字段须与数据库严格对齐;推荐中间件注入 db 并统一兜底类型断言;事务必须用 tx.runintx 确保自动 rollback。

直接说结论:Bun 不是 GORM,Gin 整合 Bun 的关键不是“连上就行”,而是绕过 Gin 默认中间件对 context.Context 的覆盖、显式管理 Bun 的 *bun.DB 生命周期、避免用 c.Request.Context() 传 DB 实例。
为什么不能直接把 *bun.DB 塞进 c.Request.Context()
Bun 的 *bun.DB 是线程安全的,但它的 query 方法(如 db.NewSelect().Model(&u).Scan(ctx, &u))**必须传入带超时/取消语义的 context.Context**;而 Gin 的 c.Request.Context() 是 HTTP 请求生命周期上下文,它会在请求结束时被 cancel——这会导致 Bun 查询中途被中断,报错 context canceled,尤其在慢查询或并发高时频繁出现。
- 错误写法:
c.Request.Context()直接传给 Bun 查询,没做任何封装 - 正确做法:用
context.WithTimeout(c.Request.Context(), 5*time.Second)包一层,或统一用context.Background()(仅限后台任务) - 更稳妥方案:在 handler 入口就提取并包装好 context,比如
ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second); defer cancel()
db.NewSelect().Where(...) 查询不返回错误但结果为空?
这是 Bun 最容易让人困惑的行为:它不像 GORM 那样在 Scan() 失败时返回非 nil error,而是只在底层 SQL 执行失败(如连接断开、语法错)时才报错;WHERE 条件不匹配、表为空、字段类型不一致导致扫描失败,都只会让 RowsAffected 为 0,err == nil。
- 必须检查
result.RowsAffected(),例如:if err == nil && result.RowsAffected() == 0 { c.JSON(404, gin.H{"error": "not found"}) } - 结构体字段类型要和数据库列严格对齐:PostgreSQL 的
jsonb字段对应 Go 的json.RawMessage或自定义 struct,不能用string - 使用
db.NewSelect().Limit(1).Scan()替代First()类方法,避免隐式行为差异
如何在 Gin 中安全注入 *bun.DB 实例
不要全局变量裸奔,也不要靠 init 函数硬塞。推荐用 Gin 的 gin.Engine.Use() 中间件 + c.Set() 组合,配合依赖注入风格初始化:
- 启动时创建
*bun.DB并配置连接池:db := bun.NewDB(sqlDB, dialect),调用db.SetMaxOpenConns(20)和db.SetMaxIdleConns(10) - 写一个中间件:
func DBMiddleware(db *bun.DB) gin.HandlerFunc { return func(c *gin.Context) { c.Set("db", db); c.Next() } } - 在路由中取用:
db, _ := c.MustGet("db").(*bun.DB),注意这里不做类型断言失败 panic 处理会炸,建议封装成getDB(c)函数统一兜底 - 别在 handler 里 close
*bun.DB—— 它是单例,应在main()退出前调用db.Close()
事务里嵌套 HTTP 错误响应,怎么避免 panic
Bun 的事务要求显式 tx.Commit() 或 tx.Rollback(),而 Gin 的 c.AbortWithStatusJSON() 不会自动 rollback。一旦你在事务里调了 c.AbortWithStatusJSON(400, ...) 后继续执行,后续 tx.Commit() 就可能 panic(因为 tx 已失效)。
- 所有事务操作必须包裹在
defer func() { if r := recover(); r != nil { tx.Rollback() } }()里(不推荐) - 更干净的做法:用
if err := tx.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.IDB) error { ... }),Bun 自动处理 commit/rollback - 关键点:事务内的所有错误路径(包括参数校验失败、业务逻辑 reject)都要统一 return error,由 Bun 的
RunInTx捕获并 rollback
真正难的不是写通第一条查询,而是让 Bun 在 Gin 的 context 生命周期、错误传播链、并发模型里不掉链子——它没有 GORM 那套“默认行为兜底”,每一步都要你亲手确认超时、扫描、事务边界和资源释放。











