如何在 mgo 中为指针字段定制 BSON 序列化行为

本文介绍如何通过类型别名配合 `setbson`/`getbson` 方法,实现对结构体指针字段(如 `*tool`)的差异化 bson 序列化,避免默认内联嵌套,转而仅存储 id 等精简引用。

在使用 mgo 驱动操作 MongoDB 时,结构体字段无论是否为指针(如 Tool vs *Tool),只要其底层类型相同,默认都会被统一内联序列化为嵌套 BSON 文档。这在需要区分“值内联”与“引用存储”(例如仅保存 _id 字符串)的场景下会造成困扰——因为 SetBSON/GetBSON 接口作用于类型本身,而非指针或非指针形态。

根本原因:mgo 的 marshalling 机制基于 Go 类型系统,*Tool 和 Tool 是两种不同类型,但 *Tool 的 SetBSON 方法若未显式定义,将无法触发自定义逻辑;而直接为 Tool 实现 SetBSON 又会同时影响值和指针两种使用方式,失去区分能力。

推荐解决方案:引入语义化类型别名

通过定义一个新类型(如 SelectiveTool)作为 Tool 的别名,并为其指针实现专属的 SetBSON 和 GetBSON,即可精准控制指针字段的序列化行为:

type Tool struct {
    ID   bson.ObjectId `bson:"_id,omitempty"`
    Name string        `bson:"name"`
    // ... other fields
}

// SelectiveTool 专用于需“按引用序列化”的场景(如关联 ID)
type SelectiveTool Tool

// SetBSON 控制 *SelectiveTool 如何从 BSON 解码(例如只读取 _id 并查库/构造)
func (st *SelectiveTool) SetBSON(raw bson.Raw) error {
    var id bson.ObjectId
    if err := raw.Unmarshal(&struct{ ID bson.ObjectId }{ID: id}); err != nil {
        return err
    }
    // 此处可按需加载完整 Tool 实例,或仅保留 ID 供后续使用
    *st = SelectiveTool{ID: id}
    return nil
}

// GetBSON 控制 *SelectiveTool 如何编码为 BSON(仅输出 _id)
func (st *SelectiveTool) GetBSON() (interface{}, error) {
    if st == nil {
        return nil, nil
    }
    return struct{ ID bson.ObjectId }{ID: st.ID}, nil
}

// 使用示例
type Order struct {
    ID             bson.ObjectId   `bson:"_id,omitempty"`
    Item           Tool            `bson:"item"`
    AssociatedItem *SelectiveTool  `bson:"associated_item"` // ✅ 仅存 ID,不内联完整 Tool
}

⚠️ 注意事项

  • SelectiveTool 必须是 Tool 的命名类型别名(type SelectiveTool Tool),而非类型别名(type SelectiveTool = Tool),否则方法集不继承;
  • 若需反向查库填充完整 Tool,应在 SetBSON 中调用业务逻辑(注意避免循环依赖或 DB 连接泄漏);
  • GetBSON 返回 nil 表示空指针不写入字段,符合 MongoDB 最佳实践;
  • 升级至 mongo-go-driver 后,可通过 MarshalBSON/UnmarshalBSON 或自定义 bson.Marshaler 接口获得更灵活控制,但本方案在 mgo 生态中简洁可靠。

该方案以最小侵入性实现了语义分离:Tool 代表“内联值”,*SelectiveTool 代表“轻量引用”,清晰表达设计意图,且完全兼容 mgo 的 marshalling 生命周期。