database/sql + mattn/go-sqlite3 是当前最稳的选择,因其编译期绑定c代码、性能好、事务完整、兼容久;modernc.org/sqlite纯go实现不支持wal等关键特性,生产环境易踩坑。

为什么 database/sql + mattn/go-sqlite3 是当前最稳的选择
Go 官方不自带 SQLite 驱动,必须靠第三方;mattn/go-sqlite3 是事实标准,编译期绑定 C 代码,性能好、事务支持全、兼容性久。别被 modernc.org/sqlite 吸引——纯 Go 实现目前不支持 WAL 模式、部分 PRAGMA 不生效,生产环境踩过坑的人基本都退回去了。
常见错误现象:undefined: sqlite3.Open 或 no such file or directory: libsqlite3.so,本质是没装 C 工具链或驱动没正确导入。
- 必须用
import _ "github.com/mattn/go-sqlite3"(下划线导入,触发 init) - macOS:确保已装
pkg-config和libsqlite3-dev(用 Homebrew 装sqlite3即可) - Linux CI 环境容易漏装
gcc和libsqlite3-dev,导致构建失败 - Windows 上若用 MSVC 工具链,需额外配置
CGO_ENABLED=1,否则报cannot use cgo
sql.Open 不等于连接成功,db.Ping() 才是第一道检查关
很多人写完 sql.Open("sqlite3", "test.db") 就直接查表,结果后面执行 Exec 或 Query 时才爆错——SQLite 文件路径不存在、权限不足、磁盘满,这些都延迟到首次操作才暴露。
使用场景:本地 CLI 工具、桌面应用初始化阶段、测试前的 DB 准备。
立即学习“go语言免费学习笔记(深入)”;
-
sql.Open只是创建连接池对象,不校验数据库可访问性 - 务必紧接着调
db.Ping(),并处理返回 error - 路径建议用绝对路径或显式拼接:
filepath.Join(os.TempDir(), "app.db"),避免相对路径在不同工作目录下失效 - 如果想自动建库,SQLite 默认会创建文件,但目录必须存在;
os.MkdirAll(filepath.Dir(dbPath), 0755)别漏
事务控制别只靠 db.Begin(),WAL 模式和 busy_timeout 得手动开
SQLite 默认是 DELETE 模式,多并发写入时容易遇到 database is locked。这不是代码写错了,是没调优底层行为。
性能影响明显:WAL 模式允许读写并行,busy_timeout 避免短时锁等待直接失败。
- 建库后立即执行:
_, _ = db.Exec("PRAGMA journal_mode = WAL") - 设置超时(毫秒):
_, _ = db.Exec("PRAGMA busy_timeout = 5000") - 事务内别混用
QueryRow和Exec——SQLite 不支持在同一个事务中交叉执行查询与修改(某些版本会静默失败) - 记得
tx.Commit()或tx.Rollback(),Go 不会自动回滚,defer 里写也得加 nil 判断
用 sqlite3.ErrNoData 判断空结果,别用 err == nil 做逻辑分支
查一条记录时,row.Scan() 返回 sql.ErrNoRows 是正常流程,不是异常。但有人把它当错误直接 log 并 return,导致“查不到就退出”这种反直觉行为。
使用场景:用户登录查 token、配置项读取、单条状态获取。
-
if err == sql.ErrNoRows表示“没找到”,该走默认值或初始化逻辑 -
if err != nil && err != sql.ErrNoRows才是真出问题 - 注意:SQLite 驱动返回的是
sql.ErrNoRows,不是sqlite3.ErrNoData(后者是底层常量,Go 标准库已封装) - 批量查询用
rows.Next()+rows.Err()检查最终错误,别只看第一次Next()返回值
SQLite 的“嵌入”特性很省事,但它的锁模型、事务边界、错误语义和 PostgreSQL/MySQL 差很多。最容易被忽略的是:它没有服务端进程,所有竞争都在文件系统层,所以 busy_timeout 和 journal_mode 不是可选项,是必调项。










