
本文详解如何使用 mgo 在 go 中正确建模父子文档关系——既保持结构清晰(parent 中嵌入 child 类型),又实现在数据库中分离存储(parent 引用 child id,child 独立存于 children 集合),避免字段丢失或冗余嵌套。
在使用 mgo 操作 MongoDB 时,一个常见误区是试图通过 bson:"-" 标签“屏蔽”子文档字段来实现引用式存储,但这会导致整个字段(包括 _id)在序列化时被完全忽略,无法满足“仅存 ID、独立建模”的需求。正确的做法是区分数据模型(domain model)与持久化模型(storage model),并善用 BSON 标签控制序列化行为。
✅ 推荐方案:双类型建模(推荐且清晰)
为 Child 定义两个类型:一个用于业务逻辑(含完整字段),一个专用于 Parent 中的引用(仅含 ID):
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c" bson:"c"`
}
// ChildRef 仅用于引用,不包含业务字段
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"` // 注意:此处是 ChildRef,非 Child
}插入时分别处理:
child := Child{
Id: bson.NewObjectId(),
C: "panino",
}
err := session.DB("mydb").C("children").Insert(child) // 存入 children 集合
parent := Parent{
Id: bson.NewObjectId(),
A: "Just a string",
B: ChildRef{Id: child.Id}, // 仅传递 ID
}
err = session.DB("mydb").C("parents").Insert(parent) // 存入 parents 集合,B 字段仅含 {_id: "..."}这样既保证了代码中 Parent.B 语义清晰(表示一个 Child 的引用),又确保数据库中 parents.b 仅为 ObjectId,而 children 集合完整保存 Child 全量数据。
⚠️ 替代方案:bson:",omitempty" 的适用场景
若坚持复用同一 Child 类型,可将非 ID 字段标记为 omitempty,但需谨慎:
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c,omitempty" bson:"c,omitempty"` // 仅当 C == "" 时省略
}⚠️ 注意:这仅在 C 为空值时跳过该字段;若 C 有值(如 "panino"),它仍会被嵌入到 Parent.B 中,违背“仅存引用”的目标。因此,omitempty 不适用于此场景的核心需求,仅适合可选字段的条件序列化。
? 关键总结
- bson:"-" 表示永远忽略该字段,不可用于保留 _id 而剔除其他字段;
- 真正需要的是类型级职责分离:业务模型(Child) vs 引用模型(ChildRef);
- MongoDB 原生不强制要求在引用中携带集合名(如 {"child_id": "...", "child_collection": "children"}),只要应用层约定明确(如 Parent.B.Id → children._id),简洁的 ObjectId 引用即可满足绝大多数场景;
- 后续查询时,可通过 session.DB("mydb").C("children").FindId(parent.B.Id).One(&child) 实现手动联查(mgo 不支持自动 JOIN,需应用层组装)。
通过这种设计,你既能享受 Go 类型系统的表达力,又能精准控制 MongoDB 中的数据落地形态。










