
在使用mgo驱动操作mongodb时,若为结构体错误地实现了调用自身递归反序列化的`setbson`方法,将导致无限递归并触发栈溢出(stack overflow)——这是典型的接口实现陷阱。
当你为 session 类型显式实现 SetBSON 方法,并在其中直接调用 raw.Unmarshal(rcv) 时,就无意中触发了 mgo 的递归调用链:
- Find().One(&session) 尝试将 BSON 数据反序列化为 *Session;
- 因 Session 实现了 bson.Setter 接口,mgo 会优先调用其 SetBSON 方法;
- 而该方法又调用 raw.Unmarshal(rcv) —— 这一操作再次尝试将原始 BSON 解析为 *Session,从而再次进入 SetBSON;
- 如此循环,永不终止,最终耗尽栈空间,panic 报错 runtime: goroutine stack exceeds 1000000000-byte limit。
✅ 正确做法是:仅在需要自定义反序列逻辑时才实现 SetBSON,且绝不可在其中递归调用 Unmarshal 到同一类型。对于绝大多数场景(如本例),完全无需实现 SetBSON —— mgo 默认的结构体字段映射已足够健壮。
以下是修复后的精简示例(移除危险的 SetBSON 实现):
package main import ( "fmt" "os" "gopkg.in/mgo.v2" // 注意:推荐使用新导入路径(原 labix.org/v2/mgo 已弃用) "gopkg.in/mgo.v2/bson" ) type Session struct { Id bson.ObjectId `bson:"_id"` Data map[string]interface{} `bson:"data"` } type Authen struct { Name string `bson:"name"` Email string `bson:"email"` } func main() { uri := "mongodb://localhost:27017" sess, err := mgo.Dial(uri) if err != nil { fmt.Printf("Can't connect to MongoDB: %vn", err) os.Exit(1) } defer sess.Close() collection := sess.DB("test").C("sess") a := &Authen{Name: "Cormier", Email: "cormier@example.com"} s := &Session{ Id: bson.NewObjectId(), Data: map[string]interface{}{"logged": a}, } if err = collection.Insert(s); err != nil { fmt.Printf("Insert failed: %vn", err) os.Exit(1) } var result Session if err = collection.Find(bson.M{}).One(&result); err != nil { fmt.Printf("Query failed: %vn", err) os.Exit(1) } fmt.Printf("Retrieved: %+vn", result) }
⚠️ 补充注意事项:
- labix.org/v2/mgo 已停止维护,强烈建议迁移到社区活跃分支,如 github.com/globalsign/mgo 或更现代的官方驱动 go.mongodb.org/mongo-driver/mongo;
- 若确需自定义反序列化(例如处理嵌套时间戳、加密字段等),SetBSON 内应手动解析 raw.Data 或使用 bson.Unmarshal() 到临时结构体/基础类型,再赋值给接收者字段,避免闭环调用;
- 始终启用 go vet 和静态检查工具,部分递归实现问题可在编译期被识别。
总结:接口实现需敬畏调用契约。Setter 不是“让我自己解自己”,而是“让我控制如何被解”——控制权在你,但递归出口必须由你明确切断。