GORM插件是实现gorm.Plugin接口的可复用组件,通过Register方法注册callback;而Callback是直接向schema.Callbacks注册函数。插件需db.Use()显式加载,支持跨model复用与第三方集成,callback仅适用于单model轻量钩子。

什么是 GORM 插件,它和 Callback 有什么区别
GORM v2 的插件机制本质是注册一组 Callback,但比直接操作 schema.Callbacks 更结构化、可复用。插件必须实现 gorm.Plugin 接口(含 Register(*gorm.DB) error 方法),而传统 callback 是直接往 db.Callback().Create().Before("gorm:before_create").Register(...) 里塞函数。不实现 Plugin 接口、只注册 callback 的代码,无法被 db.Use() 接受——这是最常卡住的第一步。
关键判断:如果你的逻辑需要跨多个 model 复用、依赖初始化顺序、或想被第三方集成(比如日志、审计、软删除增强),就该写插件;如果只是单个 model 的钩子,直接用 AfterFind 或 BeforeCreate tag 更轻量。
如何注册一个最简可用的插件
实现 gorm.Plugin 接口后,必须在 Register 方法中完成所有 callback 注册,且不能返回错误(除非真出错)。GORM 不会重试或兜底——注册失败会导致 db.Use() panic。
- 插件结构体本身通常不含状态,因为
*gorm.DB是每次调用时传入的,状态应存在db.Statement.Context或通过db.InstanceSet存储 -
Register中注册的 callback 函数签名必须是func(*gorm.DB),不能带额外参数 - 避免在
Register里做耗时操作(如建表、连 Redis),GORM 初始化阶段阻塞会影响整个应用启动
示例:
type TimestampPlugin struct{}
func (t TimestampPlugin) Register(db *gorm.DB) error {
db.Callback().Create().Before("gorm:before_create").Register("plugin:timestamp", func(tx *gorm.DB) {
if tx.Statement.Schema != nil {
now := time.Now()
for _, field := range tx.Statement.Schema.FieldsByDBName {
if field.DBName == "created_at" || field.DBName == "updated_at" {
tx.Statement.SetColumn(field.Name, now)
}
}
}
})
return nil
}
// 使用:
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.Use(&TimestampPlugin{})
为什么你的插件没生效:常见注册时机与作用域陷阱
插件只对调用 db.Use() 之后创建的 session 生效。如果你在 db 上注册插件,但后续用 db.Session(&gorm.Session{}) 或 db.WithContext() 创建新实例,这些新实例默认不继承已注册插件——GORM 插件不自动透传到子 session。
- 必须显式调用
childDB.Use(plugin),或在父db上注册后再派生(此时插件已加载) - 全局插件(如日志)建议在初始化
*gorm.DB后立刻Use(),不要延迟到某个 handler 里才注册 - callback 执行顺序依赖注册顺序和 GORM 内置 hook 名(如
"gorm:begin_transaction"),用Before()/After()指定锚点比用Replace()更安全 - 注意
Statement.ReflectValue可能为nil(如 Raw SQL 查询),插件中需判空,否则 panic
如何让插件支持配置和上下文感知
插件本身是无状态的,但你可以通过 db.Statement.Context 传递运行时参数,或用 db.InstanceSet/db.GetInstance 存储配置。别把配置塞进插件 struct 字段——它会被多次复用,字段值可能污染。
- 推荐方式:构造插件时传配置,存入闭包或用
sync.Map关联*gorm.DB实例(注意 key 用unsafe.Pointer或uintptr) - 若需 per-query 控制(如跳过某次审计),在 query 前设置 context:
db.WithContext(context.WithValue(ctx, skipAuditKey, true)),插件内检查tx.Statement.Context.Value(skipAuditKey) - 慎用
db.Session(&gorm.Session{PrepareStmt: true})配合插件——prepare 语句会缓存 callback 执行结果,导致配置变更不生效
复杂点在于:插件生命周期和 DB 实例强绑定,但 GORM 允许无限派生 session,而插件不会自动传播。这容易被忽略,直到你在子 session 里发现 timestamp 没更新、日志没打印——这时候得回头检查 Use() 调用位置和 session 创建链路。











