应全局复用单个*sql.DB实例,仅在启动时调用一次sql.Open;必须设置SetMaxOpenConns、SetMaxIdleConns和SetConnMaxLifetime,避免在handler中重复Open导致连接泄漏与性能下降。

sql.Open不是建连,别在handler里反复调用
很多人以为sql.Open会立刻连上数据库,其实它只做两件事:加载驱动、初始化连接池配置。真正拨号发生在第一次Query或Exec时。如果你在 HTTP handler 里每次请求都sql.Open再db.Close(),等于绕过连接池,每请求都新建连接——端口耗尽、TLS 握手堆积、goroutine 泄漏全来了。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 全局声明一个
*sql.DB变量,在main或初始化函数中调用一次sql.Open - 绝对不要在 handler、中间件、循环里重复
sql.Open -
db.Close()只应在服务退出前调用一次,不是每次查询后 - 用
db.Stats()观察OpenConnections和WaitCount,突增说明连接池被绕过了
SetMaxOpenConns和SetMaxIdleConns必须显式设
默认SetMaxOpenConns=0(无上限),SetMaxIdleConns=2,看似“省心”,实则高并发下极易打满数据库连接数或卡在等待空闲连接上。更糟的是,MySQL 默认wait_timeout=8h,而 Go 不设SetConnMaxLifetime时连接永不过期,DB 主动断连后 Go 还往里发请求,报i/o timeout或connection refused。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
-
SetMaxOpenConns按「峰值 QPS × 平均查询耗时(秒)」估算,比如 100 QPS × 0.2s = 20,就设20 -
SetMaxIdleConns设为SetMaxOpenConns的 1/2~2/3,如10,避免空闲连接长期占着 DB 资源 -
SetConnMaxLifetime(30 * time.Minute)必须加,防止 stale connection -
SetConnMaxIdleTime(30 * time.Second)建议加上,及时回收冷连接
查单行用QueryRowContext,别用Query+Scan手动控制
QueryRow和Query底层走的是一套逻辑,性能差异可忽略;但误用带来的副作用极严重。典型错误是:用Query取单行却忘了rows.Close(),导致连接卡在池中无法复用;或用QueryRow查可能为空的数据却不检查err,结果Scan返回零值,错误被静默吞掉。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 明确只取一行 → 用
QueryRowContext(ctx, ...),它自动处理Close,语义清晰 - 必须检查
QueryRowContext返回的err:可能是语法错、表不存在、连接失败,不只是sql.ErrNoRows - HTTP handler 中传
r.Context(),别用context.Background(),否则客户端取消后查询还在跑 - 批量查多 ID 时,别循环调
QueryRowContext,改用IN或JOIN,避免 N+1
批量插入优先拼VALUES,别循环Exec
逐行db.Exec("INSERT INTO t(x) VALUES (?)", x)是最常见性能杀手:每条语句都经历完整 round-trip、参数解析、计划缓存查找。PostgreSQL 和 MySQL 8.0+ 都支持单条语句插入多行,吞吐能提 5–20 倍。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
strings.Builder拼接VALUES (?, ?), (?, ?),避免fmt.Sprintf反复分配内存 - 参数总数注意限制:MySQL 受
max_allowed_packet制约,PostgreSQL 对 bind 参数个数无硬限但超 1000 行后性能明显下降 - 如果字段含
TEXT或JSON,拼接前先校验长度,防 payload 超限 - 高频固定批量写入(如日志归档),可用
db.Prepare复用 stmt,但别在循环里反复Prepare
连接池参数和上下文传递是隐形瓶颈,比 SQL 本身更容易被忽视;一旦设错,服务在压测初期就卡住,而日志里往往只看到超时,找不到根因。


















