
在使用 Go 的 database/sql 包遍历查询结果时,直接在循环中执行 UPDATE 会导致 SQLite 数据库锁冲突;正确做法是将 SELECT 与 UPDATE 操作统一置于同一个事务(*sql.Tx)中,既避免锁竞争,又提升批量更新效率。
在使用 go 的 `database/sql` 包遍历查询结果时,直接在循环中执行 update 会导致 sqlite 数据库锁冲突;正确做法是将 select 与 update 操作统一置于同一个事务(*sql.tx)中,既避免锁竞争,又提升批量更新效率。
在 Go 中操作 SQLite(如使用 mattn/go-sqlite3 驱动)时,若在未关闭 *sql.Rows 的情况下对同一数据库执行写操作(如 db.Exec),会触发 database is locked 错误。这是因为 SQLite 的默认连接模式为“串行化”,且 database/sql 的 *sql.Rows 在迭代期间会持有读锁,而独立的 db.Exec 尝试获取写锁时发生冲突。
根本解法是显式使用事务:通过 db.Begin() 获取 *sql.Tx,所有读(tx.Query)和写(tx.Exec)均在该事务上下文中执行。事务确保了隔离性与一致性,同时消除了跨语句的锁竞争。
以下是推荐的重构代码示例:
tx, err := db.Begin()
if err != nil {
log.Fatal("failed to begin transaction:", err)
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
rows, err := tx.Query("SELECT id, field1, field2 FROM table")
if err != nil {
log.Fatal("query failed:", err)
}
for rows.Next() {
var id, field1, field2 int
if err := rows.Scan(&id, &field1, &field2); err != nil {
log.Fatal("scan failed:", err)
}
// 应用业务逻辑(每行独立计算)
field1 += someFunc(id) // 示例:传入 id 增强可复现性
field2 += someOtherFunc(id)
// 在同一事务中更新当前行
_, err := tx.Exec("UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
field1, field2, id)
if err != nil {
log.Fatal("update failed:", err)
}
}
// 显式提交事务
if err := rows.Err(); err != nil {
log.Fatal("iteration error:", err)
}
if err := tx.Commit(); err != nil {
log.Fatal("commit failed:", err)
}✅ 关键优势:
-
无锁冲突:所有操作在单个事务内完成,SQLite 不会在
rows.Next()期间阻塞tx.Exec; -
原子性保障:整批处理要么全部成功,要么全部回滚(通过
defer tx.Rollback()+ 显式tx.Commit()实现); -
性能可控:相比逐条
db.Exec,事务减少了 WAL 日志刷盘与锁协商开销;内存占用仍保持 O(1),无需缓存全部数据。
⚠️ 注意事项:
- 切勿混用
db和tx对象(如用db.Query+tx.Exec),必须全程使用tx; -
rows.Close()可省略(tx.Commit()或tx.Rollback()会自动清理关联资源),但显式调用更清晰; - 若需高并发写入,应评估是否改用
UPDATE ... SET ... WHERE ...单语句批量更新(当逻辑可 SQL 化时),进一步减少 Go 层循环开销。
综上,database/sql 虽不提供游标(cursor)API,但事务机制天然支持“读-改-写”流水线——这是标准、安全且符合 SQLite 特性的最佳实践。


















