Go 中的 database/sql
database/sql 提供连接池与查询抽象;驱动实现协议。覆盖 Open、Query/Exec、Scan、事务、context 与资源关闭。
[!info] 关联笔记
Go 中的 database/sql
这个概念为什么会出现
访问数据库需要:连接管理、语句执行、结果扫描、事务、超时取消。若每个驱动 API 各异,业务代码无法移植。database/sql 提供统一门面,具体协议由 driver 实现(如 pgx 的 stdlib 适配、go-sql-driver/mysql)。
[!abstract] 一句话理解
sql.DB是连接池抽象:用Query/Exec/Tx访问数据,用Scan填入变量;务必CloseRows,并用 context 控制超时。
最小可运行示例
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/jackc/pgx/v5/stdlib" // 示例驱动,按实际替换
)
func main() {
db, err := sql.Open("pgx", "postgres://user:pass@localhost/db?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatal(err)
}
var n int
err = db.QueryRowContext(ctx, `SELECT 1`).Scan(&n)
fmt.Println(n, err)
}
驱动 import 与 DSN 随数据库变化;以所选驱动文档为准。下面查询示例用
?占位,PostgreSQL 常用$1。
func listUsers(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, `SELECT id, email FROM users WHERE active = $1`, true)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int64
var email string
if err := rows.Scan(&id, &email); err != nil {
return err
}
fmt.Println(id, email)
}
return rows.Err()
}
func createUser(ctx context.Context, db *sql.DB, email string) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // Commit 后 Rollback 为 no-op 错误可忽略模式需查文档;惯用 defer + 显式 Commit
if _, err := tx.ExecContext(ctx, `INSERT INTO users(email) VALUES ($1)`, email); err != nil {
return err
}
return tx.Commit()
}
关注点:
Open不保证连通——用PingContext。Rows必须 Close,并检查rows.Err()。- 事务路径上失败要 Rollback,成功 Commit。
核心概念与准确模型
sql.DB
- 不是单连接,而是池
- 进程内通常一个
*sql.DB共享 - 配置:
SetMaxOpenConns/MaxIdle/ConnMaxLifetime/ConnMaxIdleTime
文档:database/sql、教程:Accessing databases。
查询 API
| API | 用途 |
|---|---|
ExecContext | 无返回行(INSERT/UPDATE/DELETE) |
QueryContext | 多行 |
QueryRowContext | 至多一行 |
PrepareContext | 预编译(池内按连接) |
Scan 与 NULL
列顺序/类型需匹配。NULL 用 sql.NullString 等,或 *string。
事务
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
// ...
if err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
资源与循环 defer
在 for 里 defer rows.Close() 会堆积——包一层函数或显式 Close。
context
所有 *Context 方法支持取消与超时,应贯穿请求(来自 r.Context())。
哨兵错误
sql.ErrNoRows:QueryRow 无数据时 Scan 返回。用 errors.Is。
设计动机
标准库稳住“如何访问”,驱动竞争“访问谁”。ORM(GORM)建在这层之上,但 SQL、池、事务语义仍必须理解。
边界情况与反直觉行为
1. 占位符方言
? vs $1 vs @p1 因驱动而异——用驱动文档,不要假设可移植字符串。
2. 连接池耗尽
表现为请求阻塞/超时,而非立刻报错。查 MaxOpenConns 与是否泄漏 Rows。
3. Prepare 与池
Prepare 的语句与连接绑定策略是驱动/池细节;短连接高频场景未必总更快。
4. 取消已发出的查询
依赖驱动与数据库是否响应取消;仍应传 ctx。
常见误区
[!warning] 常见误区:忽略
Rows.Close
连接泄漏直至池枯竭。
[!warning] 常见误区:错误路径忘记
Rollback
事务悬挂或占连接。
[!warning] 常见误区:字符串拼接 SQL
注入风险;始终参数化。
[!warning] 常见误区:每次请求
sql.Open/Close
失去池化,开销巨大。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| 裸驱动(pgx 原生) | 功能更强/性能路径不同;API 不统一 |
| ORM | 生产力高;仍可能生成低效 SQL |
| 缓存 | 减少读库;一致性独立问题 |
| Repository 层 | 把 Scan/SQL 关在数据访问边界 |
工程实践
- 一进程共享
*sql.DB,在 main 注入 repository。 - 仓储封装错误翻译(唯一约束 → 领域
ErrConflict)。 - 迁移用独立工具(golang-migrate 等),勿靠启动时隐式 DDL 混用。
- 测试:集成测试 + 事务回滚或 testcontainers。
- 指标:池等待、慢查询、错误率。
- 只读副本:可第二
*sql.DB,在 repo 分流。
可验证实验
实验 1:不 Close Rows
并发查询后观察连接数/超时。
实验 2:短 timeout
WithTimeout 打断慢查询。
实验 3:ErrNoRows
QueryRow 无数据,errors.Is(err, sql.ErrNoRows)。
实验 4:事务回滚
插入后 Rollback,再查应不存在。
本节总结
- 本质:
database/sql= 池 + 查询/事务抽象。 - 关键规则:context、Close、参数化、事务边界、共享 DB。
- 最易错:泄漏 Rows、拼接 SQL、Open 当连通保证。
- 下一步:仓储分层;缓存 保护读路径。
自测题
概念题
sql.Open是否保证网络已连通?- 为何优先
QueryContext而非Query? QueryRow无行时错误是什么?
代码推理题
在 for rows.Next() 循环内 defer rows.Close() 处理嵌套查询——有何风险?
工程思考题
如何把 sql.ErrNoRows 转成 API 的 404 而不在 handler 依赖 database/sql?
参考答案
展开
- 否,通常惰性;用 Ping。
- 支持取消/超时,避免请求结束后查询仍占连接。
sql.ErrNoRows(在 Scan 时)。
代码题:defer 延迟到函数返回,连接/语句长期占用;应立即 Close 或抽函数。
工程题:repository 映射为领域ErrNotFound,handler 只认领域错误。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| database/sql | 标准库 | API |
| Accessing databases | 官方教程 | 池、查询、事务 |
| Go Blog — database | 官方 | 历史文章检索 |
| go-context · go-resource-cleanup · go-error-handling | 本库 | 取消、关闭、错误 |