Go 中的 database/sql

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

#type / concept #status / growing #tech / dev / backend #resource / go

[!info] 关联笔记

Go 中的 database/sql

这个概念为什么会出现

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

[!abstract] 一句话理解 sql.DB 是连接池抽象:用 Query/Exec/Tx 访问数据,用 Scan 填入变量;务必 Close Rows,并用 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()
}

关注点:

  1. Open 不保证连通——用 PingContext
  2. Rows 必须 Close,并检查 rows.Err()
  3. 事务路径上失败要 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

fordefer rows.Close() 会堆积——包一层函数或显式 Close。

context

所有 *Context 方法支持取消与超时,应贯穿请求(来自 r.Context())。

哨兵错误

sql.ErrNoRowsQueryRow 无数据时 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 关在数据访问边界

工程实践

  1. 一进程共享 *sql.DB,在 main 注入 repository。
  2. 仓储封装错误翻译(唯一约束 → 领域 ErrConflict)。
  3. 迁移用独立工具(golang-migrate 等),勿靠启动时隐式 DDL 混用。
  4. 测试:集成测试 + 事务回滚或 testcontainers。
  5. 指标:池等待、慢查询、错误率。
  6. 只读副本:可第二 *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 当连通保证。
  • 下一步仓储分层缓存 保护读路径。

自测题

概念题

  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/7/15