导语:database/sql 是被低估的连接池引擎
很多 Go 项目用 ORM(GORM、sqlx),以为 database/sql 只是"底层的原始接口"。其实 database/sql 内置了一个非常必要的连接池,连接复用、生命周期管理都在这里。不懂它的连接池语义,哪怕用 ORM 也会遇到"too many connections"或"连接泄漏"。
一句话总结:database/sql 的连接池是后台真金——把 MaxOpenConns、MaxIdleConns、ConnMaxLifetime 调对,比换任何 ORM 都对性能影响更大。
1. 连接池:灵魂三参数
1.1 标准初始化
import "database/sql"
import _ "github.com/go-sql-driver/mysql" // 空导入注册驱动
db, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/db?parseTime=true")
if err != nil { panic(err) } // 注意:这里只检验 DSN,不真正连库
// —— 连接池三参数 ——
db.SetMaxOpenConns(100) // 连接池最大连接数(默认无限)
db.SetMaxIdleConns(10) // 空闲连接保留数
db.SetConnMaxLifetime(0) // 连接最长存活(防数据库侧回收)
// 验证连通性(真正建立连接)
if err := db.Ping(); err != nil { log.Fatal(err) }
1.2 参数语义与风险
| 参数 | 作用 | 设错后果 |
|---|---|---|
| SetMaxOpenConns | 并发打开连接上限 | 过小排队、过大拖垮数据库 |
| SetMaxIdleConns | 空闲缓存数 | 过小频繁重建连接 |
| SetConnMaxLifetime | 连接存活上限 | 过长遇数据库重启被死连接 |
| SetConnMaxIdleTime | 空闲连接回收 | 过短浪费已建连接 |
推荐经验:
- MaxOpenConns ≈ 数据库 max_connections / 实例数
- MaxIdleConns ≤ MaxOpenConns,通常几~几十
- ConnMaxLifetime 设 60s~数分钟,配合数据库 sidecar 重启
- ConnMaxIdleTime 设 30s~数分钟
一句话总结:连接池三参数定了"上限、缓存、寿命",配错会直接表现为连接数告警或错误增多。
2. 事务:保证一致性的四段式
2.1 标准事务流程
tx, err := db.Begin() // 开启事务
if err != nil { return err }
// 关键:无论成败都要结束事务
defer func() {
if tx != nil {
_ = tx.Rollback() // 兜底回滚(若已 Commit 则无害)
}
}()
if _, err := tx.Exec(`UPDATE accounts SET balance=balance-100 WHERE id=1`); err != nil {
return err // 出错直接 return,defer 回滚
}
if _, err := tx.Exec(`UPDATE accounts SET balance=balance+100 WHERE id=2`); err != nil {
return err
}
// 全部成功后提交
if err := tx.Commit(); err != nil {
return err
}
tx = nil // 标记已提交,defer 不再回滚
return nil
2.2 事务隔离级别
// 需要指定隔离级别时
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted, // 读已提交(默认多数)
ReadOnly: false,
})
常见的隔离级别给开发带来哪些语义,参考:
ReadUncommitted:脏读
ReadCommitted :防脏读
RepeatableRead:防不可重复读(MySQL 默认)
Serializable :防幻读,性能最低
一句话总结:事务= Begin + 多步 SQL + Commit,defer Rollback 兜底,隔离级别按业务一致性与并发取舍。
3. 预编译语句与参数化防注入
3.1 Prepare 复用
// 预编译:SQL 只解析一次,参数复用
stmt, err := db.Prepare(`INSERT INTO user(name, age) VALUES(?, ?)`)
if err != nil { return err }
defer stmt.Close()
for _, u := range users {
if _, err := stmt.Exec(u.Name, u.Age); err != nil {
return err
}
}
3.2 永远参数化,不拼接
// ❌ 危险:SQL 注入
// db.Query("SELECT * FROM user WHERE name='" + name + "'")
// ✅ 参数化:占位符(不同驱动占位符不同)
// MySQL: ? PostgreSQL: $1, $2 SQLite: ?
rows, err := db.Query(`SELECT id, name FROM user WHERE age >= ?`, minAge)
if err != nil { return err }
defer rows.Close()
一句话总结:一律参数化 SQL,禁止字符串拼接——防注入同时让预编译加速重复执行。
4. 批量插入与扫描空值
4.1 批量插入写法
// 批量:逐条 Exec 在循环里会慢,用事务包裹
tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback()
stmt, _ := tx.Prepare(`INSERT INTO item(code, qty) VALUES(?,?)`)
for _, it := range items {
if _, err := stmt.Exec(it.Code, it.Qty); err != nil {
return err // 任一条失败整体回滚
}
}
_ = stmt.Close()
if err := tx.Commit(); err != nil { return err }
4.2 NULL 值的扫描
// SQL NULL 不能直接 Scan 到 string/int,需用 sql.NullString 等
var (
name string
note sql.NullString // 可为 NULL 的字符串
)
row.Scan(&name, ¬e)
if note.Valid {
fmt.Println("note:", note.String) // 有值
} else {
fmt.Println("note: NULL")
}
4.3 named arg
// 用名字绑定参数,避免数 ? 位置
db.Exec(`UPDATE t SET age=@age WHERE id=@id`,
sql.Named("age", 30),
sql.Named("id", 7),
)
一句话总结:批量插入用事务+预编译一次过,NULL 用 sql.NullTYPE 处理,参数用 Named 绑定提升可读性。
5. 常见坑与排查速查
| 症状 | 根源 | 对策 |
|---|---|---|
| too many connections | MaxOpen 太大 / 连接未归还 | 调小上限、defer rows.Close |
| 连接池耗尽死等 | 事务/rows 忘了 Close/Commit | 记得 defer rows.Close、defer Rollback |
| stale / bad connections | 数据库重启、ConnMaxLifetime 过久 | 设 ConnMaxLifetime + 重试 |
| 死锁 | 事务顺序不一致、跨库 | 统一锁顺序、缩短事务 |
| 慢 SQL 拖垮 | 全表扫描 | 加索引、explain |
| 数据不一致 | 隔离级别不够 | 按需提高隔离 / 加锁 |
生产避坑清单:
□ rows.Query 必须 defer rows.Close(),否则连接不归还
□ 每个事务都有 defer Rollback 兜底
□ 长事务拆分,别一把梭大事务
□ 用 context 给每条 SQL 设超时
□ 连接数、慢查询加监控告警
□ 批量用事务包裹,别逐条 auto-commit
□ 密码/DSN 用 env/secret,别硬编码
6. 总结
database/sql 的标准动作:
| 环节 | 要点 |
|---|---|
| 连接池 | MaxOpen/MaxIdle/ConnMaxLifetime |
| 事务 | Begin + defer Rollback + Commit |
| 防注入 | Prepared Statement 参数化 |
| NULL | sql.NullType / decimal |
| 批量 | 事务包裹、分批 insert |
| 兜底 | rows.Close、超时、监控 |
落地记住五件事:调好连接池三参数、事务必带 defer Rollback、SQL 全参数化、rows 及时 Close、NULL 用 Null 类型。把 database/sql 的连接池吃透,用 ORM 心里也有底。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。