最简路径是用database/sql搭配pgx/v5(PostgreSQL)或go-sql-driver/mysql(MySQL),注意sql.Open不建连、必须db.Ping验证、及时db.Close、防SQL注入、正确处理sql.ErrNoRows、事务用defer回滚。

用 database/sql 连 PostgreSQL 或 MySQL 最简路径
Go 没有内置 ORM,但标准库 database/sql 足够支撑初级项目的 CRUD。关键不是选多“高级”的框架,而是避开连接泄漏、SQL 注入和空指针这三类高频问题。
以 PostgreSQL 为例,推荐用 pgx/v5 驱动(比 lib/pq 更现代),MySQL 则用 go-sql-driver/mysql。两者都兼容 database/sql 接口,切换成本低。
-
sql.Open只是初始化驱动,不校验连接;真正建连发生在第一次db.Query或db.Exec - 必须调用
db.Close()—— 不然进程退出前连接不会释放,本地开发容易忽略,上线后连接数爆满 - 连接字符串里避免硬编码密码:用环境变量(如
os.Getenv("DB_URL"))或配置文件
package mainimport ( "database/sql" "log" _ "github.com/jackc/pgx/v5" )
func main() { db, err := sql.Open("pgx", "postgres://user:pass@localhost:5432/mydb") if err != nil { log.Fatal(err) } defer db.Close() // 必须放这里,不是在函数末尾随便写
err = db.Ping() // 主动触发一次连接测试 if err != nil { log.Fatal(err) }}
QueryRow和Query的边界在哪初学者常混淆单行 vs 多行查询的处理方式,导致 panic 或漏读数据。
-
db.QueryRow()用于「预期最多一行」的场景(如SELECT * FROM users WHERE id = ?),必须调用.Scan(),否则行被丢弃且无提示 -
db.Query()返回*sql.Rows,必须循环rows.Next()并在结束后调用rows.Close(),否则连接被占用 - 如果
QueryRow().Scan()找不到数据,会返回sql.ErrNoRows,不是nil错误 —— 忘判这个是线上空指针主因
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 123).Scan(&name)
if err == sql.ErrNoRows {
// 处理不存在的情况
} else if err != nil {
log.Fatal(err)
}
// name 现在可用INSERT 时如何安全获取自增 ID 或 UUID
PostgreSQL 和 MySQL 对返回插入 ID 的语法不同,硬写 SQL 容易出错。
- PostgreSQL 推荐用
RETURNING:直接在 INSERT 语句末尾加RETURNING id,再用QueryRow().Scan() - MySQL 用
LAST_INSERT_ID(),但必须确保在同一个连接上执行(db.Exec后立刻db.QueryRow("SELECT LAST_INSERT_ID()")) - 更稳妥的做法是业务层生成 UUID(如
github.com/google/uuid),INSERT 时显式传入,彻底规避数据库依赖
// PostgreSQL 示例
var newID int
err := db.QueryRow(
"INSERT INTO users(name, email) VALUES($1, $2) RETURNING id",
"Alice", "a@example.com",
).Scan(&newID)
if err != nil {
log.Fatal(err)
}事务里忘了 tx.Commit() 或 tx.Rollback() 怎么办
事务不结束,连接就卡在“in transaction”状态,后续操作可能被锁住或超时。Go 没有类似 Python 的 with 自动管理,得靠代码结构兜底。
- 不要把
tx.Commit()写在if err == nil分支末尾 —— 中间加新逻辑容易漏掉 - 统一用
defer func()匿名函数包裹回滚,再在成功时显式标记并跳过 - 所有
tx.Query/tx.Exec都必须用tx对象,不能混用db,否则不在事务内
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
if err := tx.QueryRow("INSERT ...").Scan(&id); err != nil {
tx.Rollback()
log.Fatal(err)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}实际项目中,最常被忽略的是 rows.Close() 和 db.Close() 的调用时机,以及对 sql.ErrNoRows 的显式判断 —— 这些不报编译错误,但一到并发或异常路径就暴露。










