sql.Open() 仅初始化连接池,不验证数据库可访问性,必须立即调用 db.Ping() 触发 sqlite3_open_v2 校验路径、权限与磁盘状态;DSN 需含 _foreign_keys=1、_busy_timeout=5000,WAL 模式须 Ping() 后手动 Exec("PRAGMA journal_mode = WAL"),事务中所有操作必须使用 tx.* 方法,严禁混用 db.*。

用 database/sql + github.com/mattn/go-sqlite3 是当前唯一靠谱的组合,其他方案要么不维护、要么缺 WAL / 外键 / 事务隔离等关键能力,生产环境踩坑成本远高于初期多写几行初始化代码。
为什么 sql.Open("sqlite3", "...") 之后 db.Ping() 必须立刻跟上
sql.Open 只是构造连接池对象,不检查文件路径是否存在、磁盘是否可写、目录权限是否足够。很多“数据库打不开”错误实际发生在第一次 db.Query 或 db.Exec 时,而非 Open 那一刻。
- 常见现象:
no such file or directory报在 Query 阶段,但你根本没做路径预检 - 路径必须显式处理:用
filepath.Join(os.TempDir(), "app.db")替代"app.db",避免工作目录切换导致文件写到奇怪位置 - 目录不存在会静默失败:SQLite 不会自动创建父目录,得自己调
os.MkdirAll(filepath.Dir(dbPath), 0755) -
db.Ping()是第一道健康检查,它会触发底层sqlite3_open_v2调用,真正校验可访问性
DSN 字符串里必须加的三个参数:_foreign_keys=1、_busy_timeout=5000、journal_mode=WAL
SQLite 默认关闭外键约束、用 DELETE 日志模式、无忙等待机制——这些在并发读写场景下极易出问题,不是代码逻辑错,是配置没到位。
-
_foreign_keys=1:不加这个,PRAGMA foreign_keys = ON在 DSN 解析阶段就被忽略,外键形同虚设 -
_busy_timeout=5000:避免database is locked瞬间 panic,让写操作自动重试 5 秒再报错 -
PRAGMA journal_mode = WAL必须在db.Ping()后立即执行:WAL 模式不能靠 DSN 参数启用,得手动db.Exec("PRAGMA journal_mode = WAL") - DSN 中不要出现
user:pass@这类 MySQL 风格字段,SQLite 不支持,会导致driver: bad connection
事务里混用 db.Query 和 tx.Query 是最隐蔽的 bug 来源
开了事务却还在用 db.Query,查到的数据不是事务快照,回滚后也不受影响——表面看程序跑通了,实际数据一致性早崩了。
立即学习“go语言免费学习笔记(深入)”;
- 所有语句必须走
tx.Query/tx.Exec/tx.Prepare,不能穿插db.*方法 - 别漏
defer tx.Rollback():Go 的defer在函数退出时才执行,如果中间 return 了,没 commit 就会自动回滚 - 需要强一致写入时,用
db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelExclusive}),默认DEFERRED模式只在首次读写才加锁 - 内存库要写成
"file::memory:?_foreign_keys=1",不是":memory:",否则 DSN 解析失败
CGO_ENABLED=1 和系统依赖不是可选项,是硬门槛
mattn/go-sqlite3 是 C 绑定驱动,禁用 CGO 或缺失头文件/库文件时,编译直接失败,不是运行时报错。
- Alpine 容器:必须
apk add --no-cache musl-dev sqlite-dev,且构建命令加CGO_ENABLED=1 - macOS:Homebrew 装完
sqlite3后,确认pkg-config --modversion sqlite3能返回版本,否则go build找不到头文件 - Windows MSVC:除了
CGO_ENABLED=1,还得确保cl.exe在 PATH 中,否则报cannot use cgo - 别被
modernc.org/sqlite吸引:纯 Go 实现目前不支持 WAL、PRAGMA busy_timeout失效、部分 VACUUM 行为异常,已有多起线上数据不一致报告
最容易被忽略的是 WAL 模式的启用时机和外键参数的 DSN 位置——它们必须在 db.Ping() 后立刻设置,而不是写在 DSN 字符串里就以为万事大吉;还有就是事务中所有操作必须严格限定在 tx 对象上,哪怕只有一行 db.Query 混进去,整个事务语义就废了。


















