17370845950

Golang初级项目如何接入数据库
用 database/sql 就够了,它轻量稳定可控,适合初学者和小项目;应避免过早使用 ORM,需手动管理连接、事务和预处理逻辑,并合理配置连接池参数。

database/sql 就够了,别急着上 ORM

Go 初级项目接入数据库,第一原则是「先跑通、再优化」。database/sql 是标准库,轻量、稳定、可控,比任何第三方 ORM 都更适合练手和小项目。ORM(比如 gormsqlx)看似省事,但隐藏了连接管理、事务边界、预处理逻辑等关键细节,出问题时反而更难定位。

实操建议:

  • 只导入驱动(如 _ "github.com/lib/pq"_ "github.com/go-sql-driver/mysql"),不引入额外抽象层
  • sql.Open 获取 *sql.DB,立刻调用 db.Ping() 验证连接是否可用
  • 连接字符串里避免硬编码密码,改用环境变量(os.Getenv("DB_URL")
  • 不要在 handler 里反复 sql.Open —— *sql.DB 本身是并发安全的、带连接池,全局复用一个实例即可

sql.QueryRowsql.Exec 要配对用好

初学者常混淆查询和执行:读数据用 QueryRow / Query,写数据(INSERT/UPDATE/DELETE)用 Exec。混用会导致 panic 或静默失败(比如对 INSERT 用 QueryRow.Scan,会报 sql: expected 1 destination arguments)。

常见错误场景:

  • INSERT 后想获取自增 ID,却用了 QueryRow("INSERT ...").Scan(&id) → 应该用 Exec + Result.LastInsertId()(MySQL)或 QueryRow("INSERT ... RETURNING id").Scan(&id)(PostgreSQL)
  • UPDATE 语句没检查 RowsAffected(),误以为更新成功 → 实际可能 where 条件没匹配到任何行
  • Query 做单行查询却不调用 rows.Next()rows.Scan() → 连接不会释放,迟早触发 too many connections
row := db.QueryRow("SELECT name FROM users WHERE id = $1", 123)
var name string
if err := row.Scan(&name); err != nil {
    // 处理 NOT FOUND 或其他 error
    return
}

事务必须显式控制,别依赖框架自动提交

Go 没有「声明式事务」机制,BeginCommit/Rollback 全靠手动。初级项目最容易漏的是 defer tx.Rollback() 的覆盖逻辑 —— 如果 Commit 成功了,还执行 Rollback 会报错(虽然不影响数据,但日志刷屏)。

正确模式:

  • tx, err := db.Begin() 开启事务
  • 所有 SQL 都调用 tx.QueryRow / tx.Exec,不是 db 本身
  • if err != nil { tx.Rollback(); return err } 出现在每个关键步骤后
  • 最后 tx.Commit() 成功才返回 nil;否则确保 Rollback 只执行一次
tx, err := db.Begin()
if err != nil {
    return err
}
defer func() {
    if p := recover(); p != nil {
        tx.Rollback()
        panic(p)
    }
}()
_, err = tx.Exec("INSERT INTO orders (...) VALUES (...)")
if err != nil {
    tx.Rollback()
    return err
}
return tx.Commit()

连接池参数不调默认值,大概率出生产事故

*sql.DB 默认最大连接数是 0(无限制),在并发稍高的服务里,数据库很快被拖垮。初级项目也得设基础水位:

  • db.SetMaxOpenConns(20):防止打爆数据库连接数
  • db.SetMaxIdleConns(5):空闲连接太多浪费资源,太少又频繁建连
  • db.SetConnMaxLifetime(30 * time.Minute):避免连接僵死(尤其云数据库有连接超时策略)

这些参数必须在 db.Ping() 之后、业务使用之前设置,否则无效。另外,别在每次 HTTP 请求里新建 *sql.DB —— 它不是轻量对象,初始化开销大,且连接池无法复用。