
本文详解如何使用 mgo 在 go 中优雅处理父子文档关系——既保持结构清晰(parent 内嵌 child 类型定义),又实现物理分离存储(parent 存引用 id,child 独立存于 children 集合),避免字段丢失或冗余序列化。
在 MongoDB 应用开发中,常需在逻辑建模与物理存储之间取得平衡:代码中希望以嵌套结构提升可读性与类型安全(如 Parent.B 直接是 Child 类型),但数据库层面又需将 Child 作为独立文档存于 children 集合,并仅在 Parent 中保存其 _id 引用——这属于典型的「引用式关系(Referenced Relationship)」,而非内嵌式(Embedded)。
关键误区在于误用 bson:"-" 标签。该标签会完全屏蔽字段的 BSON 序列化/反序列化,导致插入 Children 集合时仅 _id 被写入,其余字段(如 C)被丢弃。正确做法是利用 bson:",omitempty" ——它仅在字段值为零值(如空字符串、零 ID、nil 指针等)时跳过,而对有效数据保持完整序列化。
以下是推荐的工程化实现方案:
✅ 方案一:单类型 + 条件序列化(简洁推荐)
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c" bson:"c"` // 显式声明,不加 "-" 或 omitempty(除非业务允许空值)
}
type Parent struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
A string `json:"a" bson:"a"`
BId bson.ObjectId `json:"b_id" bson:"b_id"` // 仅存引用 ID
B *Child `json:"-" bson:"-"` // Go 层逻辑关联,不参与 BSON 编解码
}- 插入 Child 时:session.DB("mydb").C("children").Insert(child) → 全字段写入。
- 插入 Parent 时:parent.BId = child.Id,再 Insert(parent) → 仅 _id 和 b_id 存入 parents 集合。
- 查询时手动 FindId(parent.BId).One(&child) 关联(或使用聚合 $lookup)。
✅ 方案二:双类型隔离(类型安全更强)
定义专用引用类型,彻底解耦序列化行为:
// 实际存储的完整 Child 文档
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c" bson:"c"`
}
// 仅用于 Parent 中的轻量引用(无额外字段,防误序列化)
type ChildRef struct {
Id bson.ObjectId `json:"_id" bson:"_id"`
}
type Parent struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
A string `json:"a" bson:"a"`
B ChildRef `json:"b" bson:"b"` // 只存 ID,BSON 层清晰可控
}此方式通过类型系统强制约束:Parent.B 只能是 ChildRef,无法意外携带 C 字段,大幅提升维护安全性。
⚠️ 注意事项
- 勿滥用 bson:"-":它适用于完全不需要持久化的字段(如临时计算值),而非“有时需要”的场景。
- ID 字段必须显式赋值:bson.ObjectId 非自增整数,务必调用 bson.NewObjectId() 初始化,否则为零值 ObjectIdHex(""),导致查询失败。
- 考虑索引优化:在 parents.b_id 上创建索引(db.parents.createIndex({"b_id": 1})),加速关联查询。
- mgo 已归档提示:当前社区主流已迁移至 mongo-go-driver,其 primitive.ObjectID 与结构体标签机制更现代稳定,新项目建议直接采用。
通过合理运用 BSON 标签与类型设计,你能在 Go 代码中享受面向对象的自然表达,同时在 MongoDB 中维持高性能、可扩展的引用关系模型——这才是真正的「形散神聚」。










