Go database/sql 实战:连接池、事务、批量插入与常见坑

深度讲解 Go database/sql 标准库:连接池参数(SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime)、事务与隔离级别、预编译 Prepare、批量插入、NULL 处理、防注入、扫描空值,以及生产环境常踩的坑与排查。

导语: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, &note)

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 connectionsMaxOpen 太大 / 连接未归还调小上限、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 参数化
NULLsql.NullType / decimal
批量事务包裹、分批 insert
兜底rows.Close、超时、监控

落地记住五件事:调好连接池三参数、事务必带 defer Rollback、SQL 全参数化、rows 及时 Close、NULL 用 Null 类型。把 database/sql 的连接池吃透,用 ORM 心里也有底。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. Go defer 与常见陷阱深度:闭包捕获、执行顺序、性能与资源管理
  2. Go HTTP/2 连接池与复用实战:Transport 调优、Keep-Alive、连接泄漏排查
  3. Go JSON 序列化性能优化:标准库、jsoniter、easyjson 对比与工程实践