← [ BACK_TO_NOTES ]

Go 中的 database/sql

database/sql 提供连接池与查询抽象;驱动实现协议。覆盖 Open、Query/Exec、Scan、事务、context 与资源关闭。

#concept #growing #backend #go

[!info] Related notes

Go 中的 database/sql

这个概念为什么会出现

访问数据库需要:连接管理、语句执行、结果扫描、事务、超时取消。若每个驱动 API 各异,业务代码无法移植。database/sql 提供统一门面,具体协议由 driver 实现(如 pgx 的 stdlib 适配、go-sql-driver/mysql)。

[!abstract] 一句话理解 sql.DB 是连接池抽象:用 Query/Exec/Tx 访问数据,用 Scan 填入变量;务必 Close Rows,并用 context 控制超时。

最小可运行示例

需要可访问的 PostgreSQL、已初始化的 Go module 和 pgx 驱动:go get github.com/jackc/pgx/v5/stdlib。通过环境变量 DATABASE_URL 提供所选环境的 DSN;TLS 参数由部署环境决定。

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"os"
	"time"

	_ "github.com/jackc/pgx/v5/stdlib" // 示例驱动,按实际替换
)

func run() error {
	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		return fmt.Errorf("DATABASE_URL is required")
	}
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		return 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 {
		return err
	}

	var n int
	if err := db.QueryRowContext(ctx, `SELECT 1`).Scan(&n); err != nil {
		return err
	}
	fmt.Println(n)
	return nil
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err) // run 已返回,其 defer 已执行
	}
}

运行 go run .,成功输出 1。这里 Ping 与查询共享 3 秒总预算。

驱动 import、DSN 与占位符随数据库变化;下面使用 PostgreSQL 的 $1。users 表需事先存在,示例不创建 schema。

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 后返回 sql.ErrTxDone

	if _, err := tx.ExecContext(ctx, `INSERT INTO users(email) VALUES ($1)`, email); err != nil {
		return err
	}
	return tx.Commit()
}

关注点:

  1. Open 不保证连通——用 PingContext。
  2. Rows 必须 Close,并检查 rows.Err()。
  3. 事务路径上失败要 Rollback,成功 Commit。

核心概念与准确模型

sql.DB

  • 不是单连接,而是池
  • 通常每个数据库目标/连接配置共享一个 *sql.DB,可供多个 goroutine 并发使用
  • 配置:SetMaxOpenConns / MaxIdle / ConnMaxLifetime / ConnMaxIdleTime

文档:database/sql、教程:Accessing databases。

查询 API

API用途
ExecContext不消费返回行;如不带 RETURNING 的 INSERT/UPDATE/DELETE
QueryContext多行
QueryRowContextScan 取第一行,丢弃其余行;不会验证查询结果唯一性,错误延迟到 Scan
PrepareContext创建可复用 Stmt;此 ctx 仅用于准备,执行时另传 ctx

Scan 与 NULL

列顺序和目标数量需匹配,转换失败由 Scan 返回。NULL 可用 sql.NullString / sql.Null[T](Go 1.22+),或 var email *string 后调用 Scan(&email);SQL NULL 得到 nil,空字符串得到指向 "" 的非 nil 指针。直接把 NULL 扫入 string 会报错。database/sql 不会根据 struct tags 自动映射列。

事务

BeginTx 返回错误时应直接返回,不能调用可能为 nil 的 tx.Rollback()。成功后立即注册 defer tx.Rollback(),在同一 tx 上执行 SQL,最后检查 Commit 的错误;上面的 createUser 展示了完整顺序。

Tx 占用单个连接。事务期间误用 db.ExecContext 会在事务之外执行,池上限为 1 时还可能等待自己占用的连接。隔离级别是否支持取决于驱动/数据库。Commit 报错时不能把之前的结果当成已成功提交,也不应无条件重试;网络故障可能使提交结果不确定,需要幂等键或查询确认。

资源与循环 defer

在 for 里 defer rows.Close() 会堆积——包一层函数或显式 Close。

context

优先传递请求 context(如 r.Context()),它可限制等待连接等过程;执行中的 I/O 能否及时取消仍取决于驱动/数据库。传给 BeginTx 的 context 覆盖事务生命周期,取消后 database/sql 会回滚,后续 Commit 返回错误。取消不等于证明某条写入从未发生。

哨兵错误

sql.ErrNoRows:QueryRow 无数据时 Scan 返回。用 errors.Is。

设计动机

标准库稳住“如何访问”,驱动竞争“访问谁”。ORM(GORM)建在这层之上,但 SQL、池、事务语义仍必须理解。

边界情况与反直觉行为

1. 占位符方言

? vs $1 vs @p1 因驱动而异——用驱动文档,不要假设可移植字符串。

2. 连接池耗尽

表现为请求阻塞/超时,而非立刻报错。查 MaxOpenConns 与是否泄漏 Rows。

3. Prepare 与池

DB.PrepareContext 得到的 Stmt 可并发复用,换到底层新连接时由池自动准备;Tx / Conn 上创建的 Stmt 则固定在那条连接上,其所属事务/连接结束后不可再用。调用方应关闭 Stmt。是否提高性能需结合驱动和负载测量。

4. 取消已发出的查询

依赖驱动与数据库是否响应取消;仍应传 ctx。

常见误区

[!warning] 常见误区:忽略 Rows.Close
提前退出遍历而不关闭会占用连接;完整遍历到末尾且没有后续结果集时会自动关闭。成功查询后仍推荐立即 defer Close,并检查 rows.Err()。

[!warning] 常见误区:错误路径忘记 Rollback
事务悬挂或占连接。

[!warning] 常见误区:字符串拼接 SQL
值使用参数绑定;表名、列名、排序方向等通常不能用值占位符,应从明确的允许列表映射,不能直接拼用户输入。

[!warning] 常见误区:每次请求 sql.Open/Close
失去池化,开销巨大。

与相邻概念对比

概念差异
裸驱动(pgx 原生)功能更强/性能路径不同;API 不统一
ORM生产力高;仍可能生成低效 SQL
缓存减少读库;一致性独立问题
Repository 层把 Scan/SQL 关在数据访问边界

工程实践

  1. 按数据库目标共享 *sql.DB,在 main 注入 repository,服务结束时统一关闭。
  2. 仓储封装错误翻译(唯一约束 → 领域 ErrConflict)。
  3. 迁移用独立工具(golang-migrate 等),勿靠启动时隐式 DDL 混用。
  4. 测试:集成测试 + 事务回滚或 testcontainers。
  5. 指标:池等待、慢查询、错误率。
  6. 只读副本:可第二 *sql.DB,在 repo 分流。

可验证实验

2026-10-07:Go 1.26.8 / pgx v5.10.0 下示例编译通过;用受控 database/sql/driver 测试驱动验证事务开始/执行/提交失败、提交后 Rollback、QueryRow 首行/无行、NULL 扫描和池等待,go test -race 通过。这不替代真实 PostgreSQL 的连接、SQL 方言、隔离与取消集成验证;下列实验仍需在测试数据库执行。

实验 1:不 Close Rows

并发查询后观察连接数/超时。

实验 2:短 timeout

使用支持取消的驱动,验证 WithTimeout 是否及时终止慢查询,并确认服务端查询/事务状态。

实验 3:ErrNoRows

QueryRow 无数据,errors.Is(err, sql.ErrNoRows)。

实验 4:事务回滚

插入后 Rollback,再查应不存在。

本节总结

  • 本质:database/sql = 池 + 查询/事务抽象。
  • 关键规则:context、Close、参数化、事务边界、共享 DB。
  • 最易错:泄漏 Rows、拼接 SQL、Open 当连通保证。
  • 下一步:仓储分层;缓存 保护读路径。

自测题

概念题

  1. sql.Open 是否保证网络已连通?
  2. 为何优先 QueryContext 而非 Query?
  3. QueryRow 无行时错误是什么?

代码推理题

在 for rows.Next() 循环内 defer rows.Close() 处理嵌套查询——有何风险?

工程思考题

如何把 sql.ErrNoRows 转成 API 的 404 而不在 handler 依赖 database/sql?

参考答案

展开
  1. 否,通常惰性;用 Ping。
  2. 传递取消/超时,限制池等待;执行中查询的及时取消取决于驱动/数据库。
  3. sql.ErrNoRows(在 Scan 时)。
    代码题:defer 延迟到函数返回,连接/语句长期占用;应立即 Close 或抽函数。
    工程题:repository 映射为领域 ErrNotFound,handler 只认领域错误。

延伸阅读与资料来源

资料类型支撑
database/sql标准库API
Accessing databases官方教程池、查询、事务
Go Blog — database官方历史文章检索
go-context · go-resource-cleanup · go-error-handling本库取消、关闭、错误

创建于 2026/6/20 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本