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为数据库访问提供了坚实基础。在其上使用sqlx或pgx以提高易用性——绝不使用ORM。
使用sqlx或pgx时,请参考库的官方文档和代码示例以获取当前API签名。
最佳实践总结
- 使用sqlx或pgx,而非ORM —— ORM隐藏SQL,生成不可预测的查询,使调试更困难
- 查询必须使用参数化占位符——切勿将用户输入拼接到SQL字符串中
- 上下文必须传递给所有数据库操作——使用
*Context方法变体(QueryContext、ExecContext、GetContext) sql.ErrNoRows必须显式处理——使用errors.Is区分“未找到”和真实错误- 行迭代后必须关闭——在
QueryContext调用后立即使用defer rows.Close() - 切勿对不返回行的语句使用
db.Query——Query返回必须关闭的*Rows;如果忘记,连接会泄漏回池中。应使用db.Exec - 对多语句操作使用事务 —— 将相关写入包装在
BeginTxx/Commit中 - 在读取要修改的数据时使用
SELECT ... FOR UPDATE—— 防止竞态条件 - 设置自定义隔离级别 —— 当默认的READ COMMITTED不足时(例如,金融操作使用可序列化)
- 使用指针字段(
*string、*int)或sql.NullXxx类型处理可空列 - 必须配置连接池 ——
SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime、SetConnMaxIdleTime - 使用外部工具进行迁移 —— golang-migrate或Flyway,绝不使用手写或AI生成的迁移SQL
- 批量操作使用合理大小 —— 不要逐行(往返次数过多),也不要一次数百万(锁和内存问题)
- 绝不创建或修改数据库模式 —— 在玩具数据上看起来正确的模式可能会在生产负载下产生热点、锁争用或缺失索引。模式设计需要理解数据量、访问模式和生产约束,AI不具备这些
- 避免隐藏的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技能了解数据库集成测试模式






