database — 数据库

pkg/core/db 在 database/sql 上做一层薄封装:SQL 还是你写的 SQL, 但一份方言能跑三个驱动。QueryOne[Article] 这样的泛型辅助函数把行扫进结构体, WithTx 管提交与回滚。这个示例默认用临时 SQLite 文件跑完所有步骤,零外部依赖。

运行

# 零外部依赖:临时 SQLite(纯 Go,无 cgo)
go run ./examples/database

# 真库(先 make dev)
go run ./examples/database -driver postgres \
  -dsn 'postgres://matex:matex@localhost:5432/matex?sslmode=disable'
go run ./examples/database -driver mysql \
  -dsn 'matex:matex@tcp(localhost:3306)/matex?parseTime=true'

不监听端口,不是 HTTP 服务。两个标志是 -driver(默认 sqlite)和 -dsn(默认空)。不传 -dsn 时,程序在临时目录建一个一次性 SQLite 文件,退出时删掉;此时 -driver 只能是 sqlite, 否则报 -dsn is required for driver "…" 并以状态码 2 退出。

它演示了什么

可移植的建表语句

下面这段来自 examples/database/main.go。它刻意避开三种方言的差异点: 主键用 VARCHAR(64) 而不是 TEXT(MySQL 不能索引裸 TEXT 主键), 没有 AUTO_INCREMENT/SERIAL/IDENTITY, 时间戳用 VARCHAR 并由 Go 侧填充。

const schema = `CREATE TABLE IF NOT EXISTS articles (
	slug       VARCHAR(64)  NOT NULL PRIMARY KEY,
	title      VARCHAR(255) NOT NULL,
	views      INTEGER      NOT NULL DEFAULT 0,
	published  BOOLEAN      NOT NULL DEFAULT FALSE,
	note       VARCHAR(255) NULL,
	updated_at VARCHAR(64)  NOT NULL
)`

type Article struct {
	Slug      string    `db:"slug"       json:"slug"`
	Title     string    `db:"title"      json:"title"`
	Views     int64     `db:"views"      json:"views"`
	Published bool      `db:"published"  json:"published"`
	Note      *string   `db:"note"       json:"note"` // NULL → nil
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

DAO 收 db.Querier,不收 *db.DB

这是整个示例里最值得照抄的一条。同样来自 examples/database/main.go: 方法收一个 db.Querier 参数,*db.DB 和 *db.Tx 都实现它。 注意 QueryOne[T] 必须是包级函数——接口的方法不能带类型参数, 所以泛型扫描函数与 Querier 接口是一对,缺一不可。

// articleDAO is written against db.Querier rather than *db.DB, which is what
// lets the same methods serve the pool and a transaction.
type articleDAO struct{}

func (articleDAO) bySlug(ctx context.Context, q db.Querier, slug string) (*Article, error) {
	return db.QueryOne[Article](ctx, q, `SELECT * FROM articles WHERE slug = ?`, slug)
}

func (articleDAO) insert(ctx context.Context, q db.Querier, a Article, now string) error {
	_, err := db.Exec(ctx, q, `INSERT INTO articles (slug, title, views, published, note, updated_at)
		VALUES (?, ?, ?, ?, ?, ?)`, a.Slug, a.Title, a.Views, a.Published, a.Note, now)
	return err
}

func (articleDAO) count(ctx context.Context, q db.Querier) (int64, error) {
	return db.QueryScalar[int64](ctx, q, `SELECT COUNT(*) FROM articles`)
}

验证

看标准输出。每行左边是步骤标签,右边是真实数据:

go run ./examples/database
driver = sqlite

schema       OK (driver=sqlite)
scalar       count=1
query one    slug=hello-matex views=1 published=false note=<nil> updated_at=2026-…Z
no row       errors.Is(err, db.ErrNoRow) = true
tx commit    count=3
tx rollback  count=3 (unchanged)
query all    3 row(s), first=hello-matex
dao + tx     count=4 (same DAO methods, tx and pool)

几步之间的数字是对得上的:插入 1 行 → scalar count=1; 事务里插 tx-a/tx-b 两行并提交 → count=3; 第二个事务写入 tx-c 后返回一个哨兵 error → 回滚, count 仍是 3;最后 DAO 在事务里写入 tx-d 并提交 → count=4。

flowchart TD
    A["db.Open(ctx, Config{Driver, DSN})"] --> B["d.Exec / QueryOne / QueryScalar
(走连接池)"] B --> C["d.WithTx(ctx, fn)"] C --> D["fn(ctx, *db.Tx)"] D -- "返回 nil" --> E["Commit"] D -- "返回 error / panic" --> F["Rollback"] G["DAO 方法
收 db.Querier"] --> B G --> D E --> H["同一 DAO 方法
两种调用点都可用"] classDef ok fill:#f0f7f0,stroke:#3a7a3a,color:#2b2722 classDef bad fill:#fdf0f0,stroke:#a33,color:#2b2722 class E,H ok class F bad
池与事务是两条入口,DAO 只认 db.Querier 这个共同接口, 因此不需要再写一套 …Tx 孪生方法。

测试

main_test.go 用 dbtest.Start(t) 拿到一个已迁移的数据库 (默认纯 Go SQLite,可离线跑),然后断言 run 返回的 result 结构体里的具体数值:

设置 DB_TEST_DRIVER / DB_TEST_DSN 可以让同一个测试跑在真库上。

go test ./examples/database

相关

数据库概念页 三方言、错误映射、迁移与慢查询日志。 matex-setup-database 让 AI 按规范把数据库接进你的项目。 示例索引 全部 20 个可运行示例。

相关示例