关于 go-data-persistence
go-data-persistence 是面向数据库应用开发者的 Go 技能,比较 SQL 与 ORM 方案,并介绍连接池、迁移、仓储边界和服务层事务实践。
主要功能
- 01原生 SQL 与 ORM 选择建议
- 02连接池配置
- 03golang-migrate 规范
- 04仓储接口与领域错误
- 05服务层事务模式
- 06查询安全与上下文检查
适用场景
- 01选择 Go 数据访问方案
- 02配置生产环境共享连接池
- 03添加可回滚的数据库迁移
- 04组合具有原子性的多步骤操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill go-data-persistence安装后在 Claude Code 中输入/go-data-persistence确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/connection-pooling.mdmigrations.mdtransactions.md详细介绍
Go 中的数据持久化
快速参考
| 主题 | 参考资料 |
|---|---|
| 连接池内部机制、大小设置、pgx 连接池、监控 | references/connection-pooling.md |
| golang-migrate 设置、文件约定、CI/CD 集成 | references/migrations.md |
| 事务辅助工具、服务层事务、隔离级别 | references/transactions.md |
选择合适的方案
根据项目需求选择合适的工具:
| 因素 | 原生 SQL(sqlx/pgx) | ORM(Ent/GORM) |
|---|---|---|
| 复杂查询 | 首选 | 不够灵活 |
| 类型安全 | 手动实现 | 自动生成 |
| 性能控制 | 完全控制 | 有限 |
| 快速原型开发 | 较慢 | 较快 |
| 架构迁移 | golang-migrate | 内置(Ent) |
| 学习曲线 | 需要掌握 SQL | ORM API |
何时使用原生 SQL(sqlx/pgx)
- 需要全面控制查询性能和执行计划
- 领域中有复杂联接、CTE、窗口函数或递归查询
- 希望避免抽象带来的额外开销,并直接使用 PostgreSQL 功能
- 团队擅长编写和维护 SQL
- 需要
LISTEN/NOTIFY、咨询锁或COPY等高级 PostgreSQL 功能
pgx 是 Go 推荐使用的 PostgreSQL 驱动程序。它原生支持 PostgreSQL 协议,性能优于 database/sql,并且可以使用 PostgreSQL 特有的功能。需要兼容 database/sql 或使用多个数据库后端时,请使用 sqlx。
何时使用 ORM(Ent/GORM)
- 希望使用类型安全的生成式查询构建器,避免编写 SQL
- 架构以 CRUD 为主,关系简单明了
- 看重生成代码、架构即代码以及自动迁移(Ent)
- 希望快速搭建原型并快速迭代架构
新项目优先选择 Ent 而非 GORM。Ent 使用代码生成实现类型安全,提供声明式架构 DSL 和内置迁移支持,并可与 GraphQL 集成。如果团队已经熟悉 GORM,或项目规模较小,则可以选择 GORM。
连接设置
每个连接数据库的 Go 应用都需要正确配置连接池。database/sql 包会自动管理连接池,但默认设置不适合生产环境。
db, err := sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("opening db: %w", err)
}
// Connection pool configuration
db.SetMaxOpenConns(25) // Max simultaneous connections
db.SetMaxIdleConns(10) // Connections kept alive when idle
db.SetConnMaxLifetime(5 * time.Minute) // Recycle connections
db.SetConnMaxIdleTime(1 * time.Minute) // Close idle connections
// Verify connection
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("pinging db: %w", err)
}
连接池设置说明
MaxOpenConns -- 数据库的最大打开连接数。此设置可防止应用程序通过过多并发连接使数据库不堪重负。对于典型的 Web 应用,可设为约 25。计算方法:将数据库的 max_connections(减去为管理员和复制预留的连接数)除以应用程序实例数。如果数据库允许 100 个连接,有 3 个应用实例,并为管理员预留 10 个,则设为 (100 - 10) / 3 = 30。
MaxIdleConns -- 连接池中空闲时保持活跃的连接数。这些预热连接可避免每个请求都要新建连接带来的延迟。可设为约 10,或大致设为 MaxOpenConns 的 40%。设得过高会浪费数据库连接;设得过低则会导致频繁重连。
ConnMaxLifetime -- 一个连接可重复使用的最长时间。达到此时长后,连接会关闭,并在下一个请求到来时创建新连接。这有助于及时获取 DNS 变更(对于故障切换至新 IP 的云数据库尤为重要)、在只读副本之间重新平衡负载,并防止连接变得过时。通常设为 5 分钟。如果基础设施使用基于 DNS 的故障切换,则设得更短(1-2 分钟)。
ConnMaxIdleTime -- 连接空闲多长时间后关闭。这样在低流量时段可将连接释放回数据库,腾出资源。通常设为 1 分钟。此值应短于 ConnMaxLifetime。
如需使用原生 PostgreSQL 支持进行 pgx 专用连接池管理,请参阅 references/connection-pooling.md。
Repository 模式
在使用方定义 store 接口,以便进行测试。基于具体数据库驱动实现该接口。此模式可使领域逻辑与数据库解耦。
// Store interface for testability
type UserStore interface {
GetUser(ctx context.Context, id string) (*User, error)
ListUsers(ctx context.Context, limit, offset int) ([]*User, error)
CreateUser(ctx context.Context, u *User) error
}
// sqlx implementation
type PostgresUserStore struct {
db *sqlx.DB
}
func NewPostgresUserStore(db *sqlx.DB) *PostgresUserStore {
return &PostgresUserStore{db: db}
}
func (s *PostgresUserStore) GetUser(ctx context.Context, id string) (*User, error) {
var u User
err := s.db.GetContext(ctx, &u, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return &u, err
}
func (s *PostgresUserStore) ListUsers(ctx context.Context, limit, offset int) ([]*User, error) {
var users []*User
err := s.db.SelectContext(ctx, &users,
"SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2",
limit, offset,
)
return users, err
}
func (s *PostgresUserStore) CreateUser(ctx context.Context, u *User) error {
_, err := s.db.NamedExecContext(ctx,
`INSERT INTO users (id, email, name, created_at, updated_at)
VALUES (:id, :email, :name, :created_at, :updated_at)`, u)
return err
}
Model 结构体标签
使用 db 标签进行 sqlx 列映射,并将模型放在 store 附近:
type User struct {
ID string `db:"id"`
Email string `db:"email"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Sentinel 错误
定义特定于领域的错误,调用方无需导入数据库包即可检查这些错误:
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)
在 store 层将数据库错误映射为领域错误:
func (s *PostgresUserStore) CreateUser(ctx context.Context, u *User) error {
_, err := s.db.NamedExecContext(ctx, query, u)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return ErrConflict
}
return fmt.Errorf("inserting user: %w", err)
}
return nil
}
迁移
使用 golang-migrate 管理架构变更。迁移由一对 SQL 文件组成:一个用于应用变更(up),另一个用于回退变更(down)。
migrations/
├── 000001_create_users.up.sql
├── 000001_create_users.down.sql
├── 000002_add_user_roles.up.sql
└── 000002_add_user_roles.down.sql
在应用程序启动时运行迁移:
import "github.com/golang-migrate/migrate/v4"
func runMigrations(dbURL string) error {
m, err := migrate.New("file://migrations", dbURL)
if err != nil {
return fmt.Errorf("creating migrator: %w", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("running migrations: %w", err)
}
return nil
}
关键规则:始终同时编写 up 和 down 迁移;使用 IF NOT EXISTS / IF EXISTS 确保幂等;绝不修改已在生产环境中应用的迁移。完整的迁移模式、CI/CD 集成和安全迁移策略,请参阅 references/migrations.md。
事务
使用事务辅助函数确保以一致的方式处理提交和回滚。事务应在服务层管理,而非 store 层,这样才能将多个 store 操作组合成一个原子单元。
func WithTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("beginning transaction: %w", err)
}
if err := fn(tx); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("rollback failed: %v (original error: %w)", rbErr, err)
}
return err
}
return tx.Commit()
}
store 方法接受 *sql.Tx 参数,以便参与由调用方控制的事务:
func (s *OrderService) PlaceOrder(ctx context.Context, order *Order) error {
return WithTx(ctx, s.db, func(tx *sql.Tx) error {
if err := s.orderStore.CreateWithTx(ctx, tx, order); err != nil {
return fmt.Errorf("creating order: %w", err)
}
if err := s.inventoryStore.DecrementWithTx(ctx, tx, order.Items); err != nil {
return fmt.Errorf("updating inventory: %w", err)
}
return nil
})
}
有关隔离级别、死锁预防、上下文传递和测试策略,请参阅 references/transactions.md。
何时加载参考文档
遇到以下情况时加载 connection-pooling.md:
- 配置 pgx 原生连接池(
pgxpool.Pool) - 为生产工作负载调整连接池大小
- 使用云数据库、PgBouncer 或处理连接数限制
- 监控连接池健康状况和指标
遇到以下情况时加载 migrations.md:
- 首次设置 golang-migrate
- 编写新的迁移文件
- 将迁移集成到 CI/CD 管道
- 处理迁移失败或回滚
遇到以下情况时加载 transactions.md:
- 实现必须保持原子性的多步骤操作
- 设计服务层事务边界
- 选择事务隔离级别
- 调试死锁或长时间运行的事务
合并前的关卡(客观检查)
按顺序执行这些检查;不要为未通过的步骤找理由。
-
迁移
- 列出你要添加或依赖的迁移文件路径(每个版本都要包含
.up.sql和.down.sql)。 - 通过: 每个新版本在磁盘上都有一对命名一致的文件(参见 迁移)。
- 通过: 你没有改写已在任何相关环境(生产或共享开发环境)中应用的迁移内容;你添加了新版本。
- 列出你要添加或依赖的迁移文件路径(每个版本都要包含
-
查询安全
- 扫描差异,查找使用
fmt.Sprintf、+或字符串拼接构建的动态 SQL,尤其是涉及请求字段、JSON 或其他外部输入的情况。 - 通过: 所有此类查询都使用绑定参数(
$1、:name)或会生成参数化语句的 ORM/查询构建器;必须动态指定的标识符(表名/列名)使用显式允许列表,而不是用户提供的原始字符串。
- 扫描差异,查找使用
-
连接池与上下文
- 确认数据库连接池的构建(
sql.Open、pgxpool.New等)只在进程启动时执行一次,并且由多个请求共享,而不是在每个请求的处理器中创建。 - 通过: 应遵循取消/超时机制的代码路径,使用
QueryContext、ExecContext、GetContext或等效方法,而不是在与context.Context相关的工作中使用不带上下文的Query/Exec。
- 确认数据库连接池的构建(
反模式
使用字符串拼接构造查询
// BAD -- SQL injection vulnerability
query := "SELECT * FROM users WHERE name = '" + name + "'"
始终使用参数化查询($1、$2 等)或命名参数(:name)。
在处理器中泄露数据库类型
// BAD -- handler depends on sql.ErrNoRows
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
user, err := s.store.GetUser(ctx, id)
if errors.Is(err, sql.ErrNoRows) { // handler knows about sql package
http.NotFound(w, r)
return
}
}
让存储层返回领域错误(ErrNotFound),并在处理器中检查这些错误。
每个请求都新建连接
// BAD -- bypasses connection pooling entirely
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
db, _ := sql.Open("postgres", connStr) // new pool per request!
defer db.Close()
}
在启动时打开一次数据库连接,并在整个应用中共享连接池。
在生产代码中使用 SELECT *
// BAD -- fragile, breaks when columns change
err := db.GetContext(ctx, &u, "SELECT * FROM users WHERE id = $1", id)
明确列出所需的列。这样可以让查询适应架构变更,并避免获取不必要的数据。
未处理上下文取消
// BAD -- ignores context, query runs even if client disconnects
rows, err := db.Query("SELECT * FROM large_table")
始终使用 Context 变体(QueryContext、ExecContext、GetContext),并传递请求上下文,以便在调用方放弃时取消查询。
在存储层方法中使用事务
// BAD -- store controls transaction, caller cannot compose
func (s *UserStore) CreateUser(ctx context.Context, u *User) error {
tx, _ := s.db.BeginTx(ctx, nil)
// ... insert user ...
return tx.Commit()
}
由服务层管理事务,并将 *sql.Tx 传入存储层方法。正确模式请参见 references/transactions.md。