新闻中心 分类>>

Golang使用database/sql进行基础CRUD

2026-01-11 00:00:00
浏览次数:
返回列表
必须先调用sql.Open获取*sql.DB但不立即建连,首次Query/Exec才触发连接;需导入驱动、检查Open和Ping的error;Query用于多行、QueryRow用于单行;Exec执行增删改并获取结果;事务须显式Commit/Rollback。

如何用 database/sql 连接数据库并执行查询

必须先调用 sql.Open 获取 *sql.DB,但它不立即建连;真正触发连接的是第一次执行查询(如 QueryExec)。常见错误是忽略返回的 error,导致后续操作 panic。

  • sql.Open 的第一个参数是驱动名(如 "mysql""postgres"),需提前导入对应驱动(如 _ "github.com/go-sql-driver/mysql"
  • 第二个参数是数据源字符串(DSN),格式因驱动而异:"user:pass@tcp(127.0.0.1:3306)/dbname?parseTime=true"
  • 务必调用 db.Ping() 主动验证连接是否可用,否则首次查询失败时才暴露问题
db, err := sql.Open("mysql", "root:@tcp(localhost:3306)/test")
if err != nil {
    log.Fatal(err)
}
if err := db.Ping(); err != nil {
    log.Fatal("failed to connect:", err)
}

QueryQueryRow 的区别与选用场景

Query 用于返回多行结果(如 SELECT 多条记录),返回 *sql.RowsQueryRow 专为「预期只有一行」设计(如 SELECT ... LIMIT 1 或聚合函数),自动调用 Scan 并处理 sql.ErrNoRows

  • QueryRow 时,即使 SQL 本意是查一行,若实际无结果,Scan 会返回 sql.ErrNoRows,不是 nil
  • Query 必须显式调用 rows.Close(),否则连接不会归还到连接池——这是最常被漏掉的资源泄漏点
  • 不要对同一 *sql.Rows 多次调用 Scan;每次 Next() 移动游标,需在循环中配对使用
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 123).Scan(&name)
if err == sql.ErrNoRows {
    // 处理未找到
} else if err != nil {
    // 处理其他错误
}

插入、更新、删除必须用 Exec,不能用 Query

Exec 专门处理不返回结果集的操作(INSERT/UPDATE/DELETE),它返回 sql.Result,可获取影响行数或最后插入 ID;用 Query 执行这类语句虽可能不报错,但会浪费连接、阻塞连接池,且无法获取执行结果。

  • Exec 不支持查询缓存,也不走查询分析器优化路径,纯命令执行
  • 获取自增 ID 要用 result.LastInsertId(),但注意:SQLite 和某些 MySQL 配置下该值可能不可靠,PostgreSQL 则根本不支持,得改用 RETURNING 子句配合 QueryRow
  • 批量插入别拼 SQL 字符串,用 VALUES (?, ?), (?, ?) 占位符 + 多参数传入,避免 SQL 注入和语法错误
res, err := db.Exec("INSERT INTO users(name, email) VALUES(?, ?)", "Alice", "a@example.com")
if err != nil {
    log.Fatal(err)
}
id, _ := res.LastInsertId() // 注意:PostgreSQL 不适用

事务处理必须显式 CommitRollback

db.Begin() 返回的 *sql.Tx 是独立连接,所有操作都绑定在其上;忘记 CommitRollback 会导致连接长期占用、锁表、甚至连接池耗尽。

立即学习“go语言免费学习笔记(深入)”;

  • 推荐用 defer tx.Rollback() 开头,再在成功路径末尾 tx.Commit() —— 这样能确保无论中间哪步出错,事务都会回滚
  • 事务内不能再用原始 *sql.DB 执行语句,否则会脱离事务上下文
  • 事务超时由驱动和数据库决定,database/sql 本身不提供超时控制,需靠上下文(context.WithTimeout)传入 BeginTx
tx, err := db.Begin()
if err != nil {
    return err
}
defer tx.Rollback()

_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1) if err != nil { return err } return tx.Commit()

事务的边界容易被忽视:比如在函数里开了事务,但调用方没处理 panic,defer 就不会触发 Rollback;更稳妥的做法是把 Rollback 放在 recover 之后,或者统一用带 context 的 BeginTx 配合超时兜底。

搜索