mcpField
G
go-data-persistence

go-data-persistence

github.com/existential-birds数据与数据库8587 次安装11 周前更新
代码仓库

关于 go-data-persistence

go-data-persistence 是面向数据库应用开发者的 Go 技能,比较 SQL 与 ORM 方案,并介绍连接池、迁移、仓储边界和服务层事务实践。

主要功能

  1. 01原生 SQL 与 ORM 选择建议
  2. 02连接池配置
  3. 03golang-migrate 规范
  4. 04仓储接口与领域错误
  5. 05服务层事务模式
  6. 06查询安全与上下文检查

适用场景

  1. 01选择 Go 数据访问方案
  2. 02配置生产环境共享连接池
  3. 03添加可回滚的数据库迁移
  4. 04组合具有原子性的多步骤操作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill go-data-persistence

安装后在 Claude Code 中输入/go-data-persistence确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
connection-pooling.md
migrations.md
transactions.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)
学习曲线需要掌握 SQLORM 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:

  • 实现必须保持原子性的多步骤操作
  • 设计服务层事务边界
  • 选择事务隔离级别
  • 调试死锁或长时间运行的事务

合并前的关卡(客观检查)

按顺序执行这些检查;不要为未通过的步骤找理由。

  1. 迁移

    1. 列出你要添加或依赖的迁移文件路径(每个版本都要包含 .up.sql 和 .down.sql)。
    2. 通过: 每个新版本在磁盘上都有一对命名一致的文件(参见 迁移)。
    3. 通过: 你没有改写已在任何相关环境(生产或共享开发环境)中应用的迁移内容;你添加了新版本。
  2. 查询安全

    1. 扫描差异,查找使用 fmt.Sprintf、+ 或字符串拼接构建的动态 SQL,尤其是涉及请求字段、JSON 或其他外部输入的情况。
    2. 通过: 所有此类查询都使用绑定参数($1、:name)或会生成参数化语句的 ORM/查询构建器;必须动态指定的标识符(表名/列名)使用显式允许列表,而不是用户提供的原始字符串。
  3. 连接池与上下文

    1. 确认数据库连接池的构建(sql.Open、pgxpool.New 等)只在进程启动时执行一次,并且由多个请求共享,而不是在每个请求的处理器中创建。
    2. 通过: 应遵循取消/超时机制的代码路径,使用 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。

评论

支持 Markdown