
在 mgo(go 的 mongodb 驱动)中,由于其非 orm 本质,需手动管理文档间关系;推荐使用 objectid 引用 + 辅助方法的方式,兼顾数据一致性、查询灵活性与结构可读性。
在 MongoDB 这类文档型数据库中,“关系”并非通过外键强制约束,而是由应用层设计决定。mgo 作为底层驱动,不提供自动关联加载(如 ActiveRecord 的 belongs_to 或 GORM 的 Preload),因此开发者需主动权衡嵌入(embedding)与引用(referencing)两种模式。
你提出的两种方式正代表了这一权衡的核心:
第一种(嵌入式 Friends []User):将完整用户文档直接存入 friends 字段。优点是单次查询即可获取全部数据;缺点是数据冗余、更新不一致风险高(如好友改名需同步所有引用处),且违背单一数据源原则,不适用于动态或大型关系网络。
第二种(引用式 Friends []bson.ObjectId):仅存储好友的 _id,轻量、一致、易维护。但如你所察,结构体失去语义表达力——Friends []bson.ObjectId 无法直观体现“这是指向 User 集合的引用”,也缺乏便捷的数据获取能力。
✅ 最佳实践:组合策略(推荐)
采用私有引用字段 + 公开业务方法,既保持数据模型简洁,又提升代码可读性与复用性:
type User struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
Username string `json:"username" bson:"username"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
friends []bson.ObjectId `bson:"friends"` // 私有字段,明确表示为引用
}
// Friends 返回当前用户的所有好友文档(按 ID 查询)
func (u *User) Friends(session *mgo.Session) ([]User, error) {
var users []User
err := session.DB("your_db").C("users").Find(bson.M{"_id": bson.M{"$in": u.friends}}).All(&users)
return users, err
}
// AddFriend 添加好友引用(仅存 ID,不嵌入完整文档)
func (u *User) AddFriend(session *mgo.Session, friendID bson.ObjectId) error {
return session.DB("your_db").C("users").UpdateId(u.Id, bson.M{
"$addToSet": bson.M{"friends": friendID},
})
}? 关键注意事项:
- 会话管理:Friends() 方法需传入 *mgo.Session(或更好的是封装为 *mgo.Collection),避免在结构体内持有数据库连接(违反单一职责)。
- 性能考量:频繁调用 Friends() 会产生额外查询;如需批量加载,建议在服务层统一预加载(N+1 问题规避)。
- 一致性保障:删除用户时,务必同步清理其他用户 friends 数组中的对应 ID(可通过 $pull 操作或事务(若 MongoDB ≥4.0 启用副本集))。
- 类型安全增强:可定义别名类型提升语义,例如 type UserID bson.ObjectId,再声明 friends []UserID,配合注释或 godoc 明确其业务含义。
总之,mgo 不隐藏复杂性,但正因如此,它赋予你对数据关系的完全控制力。放弃“自动关联”的幻想,拥抱显式、可控、可测试的设计——这才是 Go + MongoDB 微服务架构的稳健之道。










