mcpField
G
go-database

go-database

github.com/eduardo-sl数据与数据库83165 次安装2 周前更新
代码仓库

关于 go-database

go-database 是面向 Go 服务开发者的数据库实践技能,涵盖查询、事务、并发、性能、工具链与测试,并提供按需参考资料。

主要功能

  1. 01配置数据库连接池
  2. 02安全执行查询并处理结果
  3. 03事务与锁定模式
  4. 04SQL 工具链与迁移
  5. 05性能与测试指导

适用场景

  1. 01审查 Go 数据库访问代码
  2. 02构建事务与重试逻辑
  3. 03避免查询和连接常见问题

安装

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

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-database

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

包含的文件

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

references/
locking-and-retries.md
performance.md
query-patterns.md
testing.md
tooling.md

详细介绍

Go 数据库模式

数据库访问往往是 Go 服务复杂度最高的部分。 正确处理连接管理、事务和查询模式。

社区默认规则。如果项目技能或 Agent 配置规则明确取代了 go-database,则以该规则为准。

详细参考资料,按需加载:

  • references/query-patterns.md — 完整的查询、扫描和行处理模式,空值 处理、避免 N+1 查询、连接泄漏示例。
  • references/tooling.md — 仓储模式实现、sqlc 注释查询、迁移工具和规则。
  • references/locking-and-retries.md — FOR UPDATE/SKIP LOCKED、 乐观版本控制、重试 40001/40P01、约束错误。
  • references/performance.md — COPY 和批量插入、键集分页、连接池统计信息、 sql.Null[T]。
  • references/testing.md — 选择伪对象还是实际数据库、 testcontainers 设置、隔离策略。

仅当下方摘要不够时,才阅读参考文件。

1. 连接管理

显式配置连接池——默认情况下连接数不受限制:

func OpenDB(dsn string) (*sql.DB, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("open db: %w", err)
    }

    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(5 * time.Minute)
    db.SetConnMaxIdleTime(1 * time.Minute)

    if err := db.PingContext(context.Background()); err != nil {
        return nil, fmt.Errorf("ping db: %w", err)
    }

    return db, nil
}
设置指南
MaxOpenConns与数据库的最大连接数 / 应用实例数匹配
MaxIdleConnsMaxOpenConns 的 40-50%
ConnMaxLifetime5-10 分钟(防止负载均衡器后的连接变陈旧)
ConnMaxIdleTime1-2 分钟

2. 查询规则

  1. 仅使用参数化查询 — 将字符串拼接到 SQL 中会造成 注入漏洞,没有例外。
  2. 始终传入上下文 — 使用 *Context 变体 (QueryContext、QueryRowContext、ExecContext),确保查询遵循 取消和超时设置。
  3. 在检查错误后立即执行 defer rows.Close(),并在迭代 循环后检查 rows.Err()。
  4. 使用 errors.Is 显式处理 sql.ErrNoRows,将其映射为 ErrUserNotFound 之类的领域错误。
var user User
err := db.QueryRowContext(ctx,
    "SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email)

if errors.Is(err, sql.ErrNoRows) {
    return nil, ErrUserNotFound
}
if err != nil {
    return nil, fmt.Errorf("get user %s: %w", id, err)
}

多行迭代模式:references/query-patterns.md。

3. 事务

使用一个能确保出错时回滚的辅助函数:

func WithTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) (err error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("begin tx: %w", err)
    }
    defer func() {
        if p := recover(); p != nil {
            _ = tx.Rollback() // release the connection, then keep panicking
            panic(p)
        }
        if err != nil {
            if rbErr := tx.Rollback(); rbErr != nil && !errors.Is(rbErr, sql.ErrTxDone) {
                err = errors.Join(err, fmt.Errorf("rollback: %w", rbErr))
            }
        }
    }()

    if err := fn(tx); err != nil {
        return err
    }
    if err := tx.Commit(); err != nil {
        return fmt.Errorf("commit tx: %w", err)
    }
    return nil
}

延迟回滚会处理 fn 内部发生的 panic,否则事务会一直保持打开状态,且其连接会一直留在连接池之外,直到上下文结束。errors.Join 可让两个错误都能被检查。

对关键操作显式设置隔离级别: sql.TxOptions{Isolation: sql.LevelSerializable}。

4. 并发控制和错误

  • 在一个事务中先读后写,使用 SELECT ... FOR UPDATE;任务队列使用 FOR UPDATE SKIP LOCKED 来认领行;按一致的顺序锁定行,以避免死锁。
  • 对用户发起的编辑,使用乐观并发控制:在 WHERE 中加入 version 列,受影响行数为 0 表示发生冲突。
  • 40001(序列化失败)和 40P01(死锁)可以重试: 使用带抖动的退避策略和重试上限,重试整个事务函数。 函数不得在事务之外产生副作用。
  • 将约束冲突映射为领域错误(23505 唯一性冲突 → ErrEmailTaken → 409)。让数据库负责强制唯一性;先检查再 插入会产生竞争条件。

代码:references/locking-and-retries.md。

5. 结构和工具

  • 仓储模式: 在使用方定义接口, 使用具体的数据库访问实现该接口,并在此边界将驱动错误映射为领域错误。
  • sqlc: 对原生 SQL 项目优先使用它——从带注释的 SQL 生成类型安全的 Go 代码,在构建时捕获查询与模式不匹配的问题。
  • 迁移: 使用工具(goose、golang-migrate、atlas),每次变更对应一个迁移,在生产环境中只向前迁移,并提供 down SQL;作为独立步骤运行,而非在服务器启动时运行。

实现和示例:references/tooling.md。

6. 常见陷阱

  • **可空列:**使用 sql.NullString/sql.NullInt64 或指针 字段(*string,nil 表示 SQL NULL)。将 NULL 扫描到普通字符串中 会在运行时出错。
  • **N+1 查询:**在遍历查询结果的循环中执行查询。改用 JOIN 或批量查询(WHERE id = ANY($1))。
  • **连接泄漏:**在 Query 和 defer rows.Close() 之间的任何提前 return 都会导致连接从连接池中泄漏;对不返回行的语句调用 db.Query 也会如此;请使用 ExecContext。

各类陷阱的示例:references/query-patterns.md。

7. 性能

  • 批量写入:使用 pgx CopyFrom,或每批几百行的多行 INSERT——绝不要每行单独往返一次。
  • 使用键集游标分页(WHERE (created_at, id) < ($1, $2)),不要使用 OFFSET,因为它会扫描被跳过的行,并且在插入数据时导致结果偏移。
  • 导出 db.Stats():WaitCount 持续上升意味着瓶颈在连接池或占用连接的其他因素。
  • sql.Null[T](Go 1.22+)可替代各类型的 NullString 系列。

示例:references/performance.md。

8. 测试

服务逻辑的测试中,应伪造由服务消费的仓库接口;对真实数据库中的 SQL 进行测试,并在容器中运行数据库(使用 testcontainers,通过 //go:build integration 控制),同时运行生产环境迁移。通过每个测试使用一个事务、模板数据库或清空数据来隔离测试。 示例:references/testing.md。

验证清单

  1. 使用明确的限制配置连接池(MaxOpenConns、MaxIdleConns、生命周期)
  2. 所有查询都使用参数化占位符,绝不拼接字符串
  3. 所有 QueryContext 结果在错误检查后立即执行 defer rows.Close()
  4. 在行迭代循环结束后检查 rows.Err()
  5. 使用 errors.Is 显式处理 sql.ErrNoRows
  6. 事务使用能确保出错时回滚的辅助函数
  7. 将上下文传递给所有数据库调用(*Context 变体)
  8. 可空列使用 sql.NullString / sql.NullInt64 或指针类型
  9. 不采用 N+1 查询模式——使用 JOIN 或批量查询
  10. 迁移有版本号、可逆转,并与应用启动分开运行
  11. 大型表不使用 OFFSET 分页;批量写入需分批处理
  12. 对照真实数据库测试 SQL,而不是模拟查询字符串

评论

支持 Markdown