golang-database

golang-database

热门

Go数据库访问综合指南——参数化查询、结构体扫描、可空列、事务、隔离级别、SELECT FOR UPDATE、连接池、批量处理、上下文传播及迁移工具。适用于编写、审查或调试与PostgreSQL、MariaDB、MySQL或SQLite交互的Golang代码;用于数据库测试;或关于database/sql、sqlx、pgx的问题。不生成数据库模式或迁移SQL。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-database
描述

Go数据库访问综合指南——参数化查询、结构体扫描、可空列、事务、隔离级别、SELECT FOR UPDATE、连接池、批量处理、上下文传播及迁移工具。适用于编写、审查或调试与PostgreSQL、MariaDB、MySQL或SQLite交互的Golang代码;用于数据库测试;或关于database/sql、sqlx、pgx的问题。不生成数据库模式或迁移SQL。

角色: 你是一名Go后端工程师,编写安全、明确且可观测的数据库代码。你将SQL视为一等语言——不使用ORM,没有魔法——并在边界处捕获数据完整性问题,而非在应用程序深处。

模式:

  • 写入模式 —— 生成新的仓库函数、查询助手或事务包装器:遵循技能的逐步指令;在生成新代码前,启动后台代理grep代码库中现有的查询模式和命名约定。
  • 审查/调试模式 —— 审计或调试现有数据库代码:使用子代理并行扫描业务逻辑,检查缺失的rows.Close()、未参数化的查询、缺失的上下文传播以及缺失的错误检查。

社区默认。 明确覆盖samber/cc-skills-golang@golang-database技能的公司技能优先。

Go数据库最佳实践

Go的database/sql为数据库访问提供了坚实基础。在其上使用sqlxpgx以提高易用性——绝不使用ORM。

使用sqlx或pgx时,请参考库的官方文档和代码示例以获取当前API签名。

最佳实践总结

  1. 使用sqlx或pgx,而非ORM —— ORM隐藏SQL,生成不可预测的查询,使调试更困难
  2. 查询必须使用参数化占位符——切勿将用户输入拼接到SQL字符串中
  3. 上下文必须传递给所有数据库操作——使用*Context方法变体(QueryContextExecContextGetContext
  4. sql.ErrNoRows必须显式处理——使用errors.Is区分“未找到”和真实错误
  5. 行迭代后必须关闭——在QueryContext调用后立即使用defer rows.Close()
  6. 切勿对不返回行的语句使用db.Query——Query返回必须关闭的*Rows;如果忘记,连接会泄漏回池中。应使用db.Exec
  7. 对多语句操作使用事务 —— 将相关写入包装在BeginTxx/Commit
  8. 在读取要修改的数据时使用SELECT ... FOR UPDATE —— 防止竞态条件
  9. 设置自定义隔离级别 —— 当默认的READ COMMITTED不足时(例如,金融操作使用可序列化)
  10. 使用指针字段(*string*int)或sql.NullXxx类型处理可空列
  11. 必须配置连接池 —— SetMaxOpenConnsSetMaxIdleConnsSetConnMaxLifetimeSetConnMaxIdleTime
  12. 使用外部工具进行迁移 —— golang-migrate或Flyway,绝不使用手写或AI生成的迁移SQL
  13. 批量操作使用合理大小 —— 不要逐行(往返次数过多),也不要一次数百万(锁和内存问题)
  14. 绝不创建或修改数据库模式 —— 在玩具数据上看起来正确的模式可能会在生产负载下产生热点、锁争用或缺失索引。模式设计需要理解数据量、访问模式和生产约束,AI不具备这些
  15. 避免隐藏的SQL特性 —— 不要在应用程序代码中依赖触发器、视图、物化视图、存储过程或行级安全

库选择

最适合 结构体扫描 PostgreSQL专用
database/sql 可移植性,最小依赖 手动Scan
sqlx 多数据库项目 StructScan
pgx PostgreSQL(快30-50%) pgx.RowToStructByName 是(COPY、LISTEN、数组)
GORM/ent 避免 魔法 抽象化

为什么不用ORM:

  • 不可预测的查询生成——代码中看不到的N+1问题
  • 魔法钩子和回调(BeforeCreate、AfterUpdate)使调试更困难
  • 模式迁移与应用程序代码耦合
  • 学习ORM API比学习SQL更难,且抽象层会泄漏

参数化查询

// ✗ 非常糟糕——SQL注入漏洞
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)

// ✓ 好——参数化(PostgreSQL)
var user User
err := db.GetContext(ctx, &user, "SELECT id, name, email FROM users WHERE email = $1", email)

// ✓ 好——参数化(MySQL)
err := db.GetContext(ctx, &user, "SELECT id, name, email FROM users WHERE email = ?", email)

动态IN子句

query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
if err != nil {
    return fmt.Errorf("构建IN子句: %w", err)
}
query = db.Rebind(query) // 根据驱动调整占位符
err = db.SelectContext(ctx, &users, query, args...)

动态列名

切勿从用户输入中插值列名。使用白名单:

allowed := map[string]bool{"name": true, "email": true, "created_at": true}
if !allowed[sortCol] {
    return fmt.Errorf("无效的排序列: %s", sortCol)
}
query := fmt.Sprintf("SELECT id, name, email FROM users ORDER BY %s", sortCol)

更多注入预防模式,请参见samber/cc-skills-golang@golang-security技能。

结构体扫描和可空列

对sqlx使用db:"column_name"标签,对pgx使用pgx.CollectRows配合pgx.RowToStructByName。使用指针字段(*string*time.Time)处理可空列——它们能干净地处理扫描和JSON序列化。参见扫描参考获取所有方法的示例。

错误处理

func GetUser(id string) (*User, error) {
    var user User

    err := db.GetContext(ctx, &user, "SELECT id, name FROM users WHERE id = $1", id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrUserNotFound // 转换为领域错误
        }
        return nil, fmt.Errorf("查询用户 %s: %w", id, err)
    }

    return &user, nil
}

或者:

func GetUser(id string) (u *User, exists bool, err error) {
    var user User

    err := db.GetContext(ctx, &user, "SELECT id, name FROM users WHERE id = $1", id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, false, nil // “无用户”不是技术错误,而是领域错误
        }
        return nil, false, fmt.Errorf("查询用户 %s: %w", id, err)
    }

    return &user, true, nil
}

始终关闭行

rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
    return fmt.Errorf("查询用户: %w", err)
}
defer rows.Close() // 防止连接泄漏

for rows.Next() {
    // ...
}
if err := rows.Err(); err != nil { // 迭代后始终检查
    return fmt.Errorf("迭代用户: %w", err)
}

常见数据库错误模式

错误 如何检测 操作
未找到行 errors.Is(err, sql.ErrNoRows) 返回领域错误
唯一约束 检查驱动特定错误码 返回冲突错误
连接被拒绝 db.PingContext返回err != nil 快速失败,记录日志,带退避重试
序列化失败 PostgreSQL错误码40001 重试整个事务
上下文取消 errors.Is(err, context.Canceled) 停止处理,传播

上下文传播

始终使用*Context方法变体来传播截止时间和取消:

// ✗ 糟糕——无上下文,即使客户端断开连接,查询也会运行完成
db.Query("SELECT ...")

// ✓ 好——尊重上下文取消和超时
db.QueryContext(ctx, "SELECT ...")

有关上下文的深入模式,请参见samber/cc-skills-golang@golang-context技能。

事务、隔离级别和锁

有关事务模式、隔离级别、SELECT FOR UPDATE和锁变体,请参见事务

连接池

db.SetMaxOpenConns(25)              // 限制总连接数
db.SetMaxIdleConns(10)              // 保持热连接就绪
db.SetConnMaxLifetime(5 * time.Minute)  // 回收过期连接
db.SetConnMaxIdleTime(1 * time.Minute)  // 更快关闭空闲连接

有关大小调整指南和公式,请参见数据库性能

迁移

使用外部迁移工具。模式变更需要人工审查,并理解数据量、现有索引、外键和生产约束。

推荐工具:

  • golang-migrate —— CLI + Go库,支持所有主流数据库
  • Flyway —— 基于JVM,在企业环境中广泛使用
  • Atlas —— 现代声明式模式管理

迁移SQL应由人工编写和审查,在源代码控制中版本化,并通过CI/CD管道应用。

避免隐藏的SQL特性

不要在应用程序代码中依赖触发器、视图、物化视图、存储过程或行级安全——它们会产生不可见的副作用,使调试变得不可能。保持SQL在Go中显式可见,以便测试和版本控制。

模式创建

本技能不涵盖模式创建。 AI生成的模式通常存在细微错误——缺失索引、列类型不正确、规范化不良或约束缺失。模式设计需要理解数据量、访问模式、查询配置文件和业务约束。请使用专用数据库工具和人工审查。

深入探讨

  • 事务 —— 事务边界、隔离级别、死锁预防、SELECT FOR UPDATE
  • 测试数据库代码 —— 模拟连接、使用容器的集成测试、夹具、模式设置/拆除
  • 数据库性能 —— 连接池大小调整、批量处理、索引策略、查询优化
  • 结构体扫描 —— 结构体标签、可空列处理、JSON序列化模式

交叉引用

  • → 参见samber/cc-skills-golang@golang-security技能了解SQL注入预防模式
  • → 参见samber/cc-skills-golang@golang-context技能了解上下文传播到数据库操作
  • → 参见samber/cc-skills-golang@golang-error-handling技能了解数据库错误包装模式
  • → 参见samber/cc-skills-golang@golang-testing技能了解数据库集成测试模式

参考