Go 高手(13):database/sql——连接池、事务、scan
更新时间:2026-09-01。本文是
languages/go/intermediate/高手层第 13 篇,接 net/http 深入。Go 标准库的database/sql提供统一的 SQL 数据库操作接口,支持连接池、事务、预编译语句。理解它的用法和底层机制,才能写出健壮的数据库操作代码。
本文要回答的问题
sql.Open和sql.DB的关系是什么?Open 会不会连接数据库?- 连接池的参数怎么配置?
SetMaxOpenConns、SetMaxIdleConns分别控制什么? Query、QueryRow、Exec分别什么时候用?- 事务怎么处理?
Tx的Commit和Rollback怎么配合? sql.Rows的遍历为什么必须Close?不 Close 会怎样?
一、sql.DB:连接池
go
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()重要: sql.Open 不会真的连接数据库,它只是初始化连接池。真正的连接在第一次查询时创建。
go
// 验证连接是否可用
if err := db.Ping(); err != nil {
log.Fatal(err)
}二、连接池配置
go
db.SetMaxOpenConns(25) // 最大打开连接数(默认无限制)
db.SetMaxIdleConns(10) // 最大空闲连接数(默认 2)
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
db.SetConnMaxIdleTime(1 * time.Minute) // 空闲最大存活时间各参数的作用:
| 参数 | 作用 | 默认值 | 建议 |
|---|---|---|---|
MaxOpenConns | 同时最多打开的连接数,超过则等待 | 无限制 | 设一个合理上限,避免压垮数据库 |
MaxIdleConns | 最多保持的空闲连接数 | 2 | 根据并发量调大 |
ConnMaxLifetime | 连接最长存活时间,到期后关闭重建 | 无限制 | 设 5-30 分钟,避免连接被中间件断开 |
ConnMaxIdleTime | 空闲连接超时关闭 | 无限制 | 配合 ConnMaxLifetime 使用 |
典型配置:
go
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)三、Query vs Exec vs QueryRow
go
// Query:返回多行结果
rows, err := db.Query("SELECT id, name FROM users WHERE age > ?", minAge)
if err != nil {
return err
}
defer rows.Close() // 必须关闭
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
return err
}
fmt.Println(id, name)
}
// 检查遍历是否出错
if err := rows.Err(); err != nil {
return err
}
// QueryRow:只返回一行
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
if err == sql.ErrNoRows {
// 没找到,不是错误
} else if err != nil {
return err
}
// Exec:不返回结果(INSERT、UPDATE、DELETE)
result, err := db.Exec("UPDATE users SET name = ? WHERE id = ?", "Alice", 1)
if err != nil {
return err
}
n, _ := result.RowsAffected() // 影响的行数三个方法的区别:
| 方法 | 返回 | 使用场景 |
|---|---|---|
Query | *Rows | SELECT 返回多行 |
QueryRow | *Row | SELECT 返回单行 |
Exec | Result | INSERT/UPDATE/DELETE |
四、预编译语句(Prepared Statement)
go
// 预编译,复用执行计划
stmt, err := db.Prepare("INSERT INTO users(name, email) VALUES(?, ?)")
if err != nil {
return err
}
defer stmt.Close()
// 多次执行,只编译一次
stmt.Exec("Alice", "alice@example.com")
stmt.Exec("Bob", "bob@example.com")好处:
- SQL 只解析一次,后续只传参数
- 防止 SQL 注入(参数被编码,不会解释为 SQL)
- 适合批量执行
五、事务
go
tx, err := db.Begin()
if err != nil {
return err
}
// 事务中的操作在同一个连接上执行
defer tx.Rollback() // 如果 Commit 失败,自动回滚
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = ?", 1)
if err != nil {
return err // 自动 Rollback
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = ?", 2)
if err != nil {
return err
}
// 全部成功,提交
return tx.Commit()事务要点:
tx.Rollback()可以多次调用,第二次调用不报错- 事务结束后(Commit 或 Rollback),
tx不能再使用 - 事务内的所有操作在同一个数据库连接上执行
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
忘记 rows.Close() | 连接泄漏,连接池耗尽 | defer rows.Close() |
rows.Err() 不检查 | 遍历中止但不知道原因 | 遍历完后检查 rows.Err() |
Scan 顺序和 SELECT 列不匹配 | 类型不匹配 panic 或数据错位 | 按 SELECT 列的顺序 Scan |
| 事务中部分操作失败忘记回滚 | 连接被占用,事务一直开着 | defer tx.Rollback() |
在循环里反复 Open 和 Close | 每次创建新连接,性能差 | 全局只 Open 一次,用连接池 |
相关与延伸
下一篇:测试进阶——mock、httptest、testify;Go 的数据库操作实践,和 C 语言数据库接口对比,见 C 数据库操作。
一句话总结
Go database/sql:sql.Open 初始化连接池,Ping 验证连接可用;SetMaxOpenConns 控制并发上限,SetMaxIdleConns 控制空闲连接;Query 返回多行,QueryRow 返回单行,Exec 执行 INSERT/UPDATE/DELETE;rows.Close() 必须 defer 调用,否则泄漏连接;事务用 Begin + Commit + Rollback,defer 自动回滚;Prepare 预编译语句防止 SQL 注入。