Go 的数据库访问以 database/sql 为中心——它是接口层,真正驱动是 pgx、modernc.org/sqlite 等。对比 JDBC + HikariCP,模型非常像,但没有 Session / 一级缓存那些 JPA 概念。
db, err := sql.Open("sqlite", path)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)
if err := db.PingContext(ctx); err != nil { ... }sql.Open 不保证连得上——一定要 Ping。池参数调不好时表现和 HikariCP 误配一样:占满、泄漏、抖动。
row := db.QueryRowContext(ctx, `SELECT id, title FROM tasks WHERE id = ?`, id)
var t Task
err := row.Scan(&t.ID, &t.Title)没有「按列名自动映射」的标准能力——sqlx 补 StructScan,pgx 走自己的 API。本教程项目 14 坚持裸 database/sql,强迫你看清扫描与生命周期。
tx, err := db.BeginTx(ctx, nil)
defer tx.Rollback() // 提交后 Rollback 是 no-op
if _, err := tx.ExecContext(ctx, ...); err != nil { return err }
return tx.Commit()ctx 取消会让驱动尽力中断查询。务必把 ctx 传到每一个 ExecContext/QueryContext。
用嵌入 SQL + 版本表,或 golang-migrate。对比 Flyway/Liquibase:Go 更常把迁移文件 embed 进二进制,启动时 apply。项目 14 用简单版本表演示这个思路。
| 工具 | 风格 | 像 Java 的 |
|---|---|---|
| GORM | 全功能 ORM | JPA/Hibernate |
| ent | Schema as code | 强类型 JOOQ 感 |
| sqlc | SQL → 生成 Go | 手写 SQL + 类型安全 |
| sqlx | 轻量扩展 | JdbcTemplate |
Go 社区对 ORM 热情低于 Java,常见理由:隐式查询难排查、接口太大、简单 CRUD 用手写更清晰。生产里 sqlc + pgx 很常见。
QueryRowContext的查询错误通常延迟到Scan返回。QueryContext返回*Rows,调用方必须关闭,并在循环后检查Rows.Err()。sql.ErrNoRows是可识别的契约,数据层可以将它映射为领域ErrNotFound。
rows, err := db.QueryContext(ctx, query, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
// Scan
}
return out, rows.Err()事务函数应将业务上必须同时成功的操作放在一起。不要为了“以后可能有用”就让所有查询共享一个长事务,这会扩大锁和连接占用时间。
项目 14 的用户、项目、首个任务属于一个完整用例,因此放在同一事务中。
教学项目可以用一份幂等 SQL 说明基本思路。当版本增多时,需要考虑:
- 版本是否只能向前
- 失败后如何恢复
- 多实例是否会同时执行迁移
- DDL 在当前数据库中是否支持事务
这些是 Flyway/Liquibase 之类工具的价值,不必在一个练习里重写它们。
- 故意让事务的第三次 INSERT 失败,验证前两次没有留在数据库。
- 为
SetTaskDone增加并发更新测试,观察 SQLite 锁行为。 - 将
:memory:替换为临时文件,证明关闭后数据是否持久化。
配套项目:14 待办 API