mongo.connect()必须传带超时的context,正确做法是用context.withtimeout控制连接耗时并设置connecttimeout/sockettimeout;struct字段须加bson tag以匹配mongodb字段名;client需全局复用,collection无需缓存;cursor必须显式close()防止goroutine泄漏。

mongo.Connect() 必须传带超时的 context,不能用 context.TODO()
直接调用 mongo.Connect(context.TODO(), opts) 看似能跑通,但生产环境一压就崩。它会让连接卡死在 DNS 解析、网络不可达或 MongoDB 服务未响应时,直到系统级超时(可能长达数分钟),且无法被上层主动 cancel。
正确做法是显式设置连接超时和 socket 超时,并绑定到请求生命周期或应用启动上下文:
- 用
context.WithTimeout(context.Background(), 10*time.Second)控制整个连接建立耗时 - 通过
options.Client().SetConnectTimeout(5*time.Second).SetSocketTimeout(5*time.Second)细粒度控制底层 TCP 行为 - 如果集成在 Echo 启动流程中,应把
*mongo.Client作为依赖注入,而非每次请求都重连
结构体字段必须加 bson tag,否则写入 MongoDB 是空文档
Go 的 struct 默认导出字段才能被 mongo-go-driver 序列化,但光导出不够——驱动默认按字段名(首字母大写)映射,而 MongoDB 文档键是小写加下划线(如 user_name)。不加 bson tag 就会写成 {"UserName": "alice"},跟数据库实际 schema 对不上。
示例错误写法:
type User struct {
ID primitive.ObjectID `json:"id"`
UserName string `json:"user_name"`
}
正确写法(注意 bson 和 json 分开控制):
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
UserName string `bson:"user_name" json:"user_name"`
}
-
bson:"user_name"决定存进 MongoDB 的字段名 -
omitempty对_id很关键:插入时跳过空值,避免手动设零值引发错误 - 嵌套结构、数组、时间字段也要显式标注,比如
CreatedAt time.Time `bson:"created_at"`
Collection 不用缓存,但 Client 必须全局复用
有人习惯在每个 handler 里写 client.Database("blog").Collection("posts"),这没问题;但更常见的是误以为要“缓存” Collection 实例来提升性能。其实 *mongo.Collection 是轻量对象,本身不含连接资源,每次获取开销极小。
真正要复用的是 *mongo.Client:
-
Client管理连接池、认证状态、监控指标,重复创建会导致连接泄漏、认证失败、内存暴涨 - Echo 的
echo.Context不自带 DB 注入,推荐在echo.New()后把client存进e.Use()中间件或自定义echo.Group的Context值里 - 关闭时机必须明确:通常在
os.Interrupt信号捕获后调用client.Disconnect(ctx),否则进程退出时连接不释放
游标(cursor)不 Close 会导致 goroutine 泄漏和内存持续增长
执行 collection.Find() 返回 *mongo.Cursor,它背后持有网络连接和缓冲区。不调用 Close() 就算迭代完所有文档,goroutine 仍驻留,缓冲区内存也不回收。
最简安全模式:
cursor, err := collection.Find(ctx, filter)
if err != nil {
return err
}
defer cursor.Close(ctx) // 注意:这里 defer 的 ctx 应该是 Find 时用的同一个,或至少非 canceled 状态
- 别在循环里 defer —— 每次迭代都 defer 会堆积大量延迟调用
- 如果用
cursor.All()一次性解码到 slice,依然要Close(),因为底层连接没释放 - 用
cursor.Next()手动迭代时,务必在for cursor.Next()结束后补cursor.Close(),哪怕中间break或return
容易被忽略的一点:UpdateOne/ReplaceOne/DeleteOne 这类单文档操作不返回 cursor,但 FindOne、Find 一定涉及 cursor 生命周期管理——这是 Go 驱动里最常踩的内存坑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










