用 database/sql 就够了,它轻量稳定可控,适合初学者和小项目;应避免过早使用 ORM,需手动管理连接、事务和预处理逻辑,并合理配置连接池参数。
用 database/sql 就够了,别急着上 ORM
Go 初级项目接入数据库,第一原则是「先跑通、再优化」。database/sql 是标准库,轻量、稳定、可控,比任何第三方 ORM 都更适合练手和小项目。ORM(比如 gorm 或 sqlx)看似省事,但隐藏了连接管理、事务边界、预处理逻辑等关键细节,出问题时反而更难定位。
实操建议:
- 只导入驱动(如
_ "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.QueryRow 和 sql.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 没有「声明式事务」机制,Begin → Commit/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 —— 它不是轻量对象,初始化开销大,且连接池无法复用。








