先把边界划死,再学用法。SQLite 被误用(拿它扛高并发写)和被埋没(明明该用却上了 MySQL)一样常见。
| 组件 | 锁定版本 | 发布日期 | 说明 |
|---|---|---|---|
| SQLite 引擎 | 3.53.4 | 2026-07-24 | 官方 amalgamation 稳定版;Go 驱动内嵌的就是这份 C 源码的翻译/绑定,不用单独装引擎 |
| modernc.org/sqlite | v1.60.1 | 2026-09-29 | 纯 Go database/sql 驱动(cc 翻译的 SQLite),免 CGO |
| glebarez/sqlite | v1.11.0 | 2024-03-14 | 包装 modernc 的 GORM 驱动,工作台默认 DB_DRIVER=sqlite 走它 |
| mattn/go-sqlite3 | v1.14.52(备选) | 2026-09-05 | CGO 驱动,性能上限略高但交叉编译麻烦;本教程场景不首选 |
| GORM | v1.31.2(v2 API 线) | 2026-06-22 | 工作台 ORM;注意模块 tag 是 v1.x 序列、API 是 v2 |
版本纪律:以上版本全部来自全系列锁定版本清单(09 模块 §4),照抄即可,不要自己去"更新到最新"。Go 侧的 SQLite 是随驱动二进制一起发布的——你锁了 modernc v1.60.1,就等于锁了它内嵌的那份 SQLite 引擎版本,服务器上不需要再装任何 sqlite3 软件包。
DB_DRIVER=mysql 开关。SQLite 的"安装"比任何数据库都简单:它不是服务,没有 systemd unit,没有监听端口。你只需要两样东西之一——命令行工具(手动玩库用),或 Go 驱动(程序用,内嵌引擎)。
sqlite3.exe(zip 包,解压即用),放进一个已加入 PATH 的目录(如 C:\Tools\sqlite3\)。不需要安装程序、不需要管理员权限。# 方式 A:验证命令行工具 sqlite3 -version # 预期输出类似:3.53.4 ... x64 sqlite3 D:\data\workbench.db # 进入交互提示符后: sqlite> SELECT sqlite_version(); # 预期:3.53.4 sqlite> .quit
# Debian/Ubuntu(一次性,仅排查用) sudo apt update && sudo apt install -y sqlite3 sqlite3 --version # 预期:3.53.x(发行版打包版本可能略旧,不影响程序,程序用的是驱动内嵌版本)
关键认知:服务器上 apt install sqlite3 装的命令行工具,和你程序实际使用的 SQLite 引擎是两份独立的代码——前者只是给你手敲 SQL 排查用;后者是 Go 二进制内嵌的那一份。不要以为"服务器装了 sqlite3 包"就等于"程序用的是系统版本"。
只讲让你"不踩坑"的那部分理论。教科书式的 B 树细节、页结构演进,等阶段四再补。
workbench.db)。备份 = 复制这个文件(见 §8 的热备份方式);迁移 = 拷贝这个文件;删除 = 删文件。workbench.db-wal(预写日志)和 workbench.db-shm(共享内存索引)两个旁支文件——这是正常的,不要删;正常关闭/检查点后 WAL 会合并回主库。SQLite 是动态类型:列声明的类型只是"亲和倾向",不是硬约束。你声明 age INTEGER,真塞进去一个字符串它也存得下(按亲和规则做转换尝试)。所以:数据合法性靠你的程序(和 GORM)把关,不要指望数据库拦你。建表时仍要写合理的类型(INTEGER / TEXT / REAL / BLOB),这是为了走对索引亲和,不是为了强校验。
| 模式 | 行为 | 取舍 |
|---|---|---|
| delete(默认) | 写操作前回滚日志覆盖整个库文件;读写互斥 | 兼容性最好;但读时写会被堵、写时读会被堵,并发差 |
| WAL(本教程采用) | 写先追加到 -wal 文件,读走主库;读写不互斥 | 读并发明显更好;写仍全局单写者;崩溃后可从 WAL 恢复 |
记住这句:WAL 提升的是"读并发",不是"写并发"。两个写事务仍然只能排队进行。看到 database is locked 时,先怀疑是不是有写事务没提交/没回滚,而不是"我没开 WAL"。
| PRAGMA | 本教程取值 | 作用 |
|---|---|---|
journal_mode | WAL | 开启预写日志,读写不互斥(见 3.3) |
busy_timeout | 5000(毫秒) | 遇到锁时等待最多 5 秒而不是立刻报 SQLITE_BUSY——低配服务器消峰的关键 |
foreign_keys | ON | SQLite 默认不启用外键约束!必须每条连接显式打开,否则 REFERENCES 形同虚设 |
synchronous | NORMAL | WAL 下 NORMAL 是官方推荐:断电最多损坏最后一次事务,崩溃不丢库;比 FULL 快很多 |
三条连接级铁律:① 每个新连接都要执行这组 PRAGMA(WAL 是库级持久的,但 busy_timeout / foreign_keys 是连接级,连接池里每个连接都得设到);② 外键不开 = 你写的 REFERENCES 全是摆设;③ busy_timeout 不设 = 偶发锁直接报错给用户。
VACUUM INTO 见 §8。语法与 MySQL 高度相似(你有 ThinkPHP 底子),只列差异点和高频命令。
-- DDL:建表(注意类型亲和,INTEGER PRIMARY KEY = 自增主键) CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, note TEXT DEFAULT '', done INTEGER NOT NULL DEFAULT 0, -- SQLite 无原生 BOOLEAN,用 0/1 created_at TEXT NOT NULL, -- 时间用 TEXT 存 RFC3339,Go 侧 time.Time 自动映射 updated_at TEXT NOT NULL ); -- DML INSERT INTO tasks (title, note, done, created_at, updated_at) VALUES ('写周报', '周五前', 0, '2026-10-14T09:00:00+08:00', '2026-10-14T09:00:00+08:00'); SELECT id, title, done FROM tasks WHERE done = 0 ORDER BY id DESC LIMIT 20; UPDATE tasks SET done = 1, updated_at = '2026-10-14T10:00:00+08:00' WHERE id = 1; DELETE FROM tasks WHERE id = 1; -- 事务(写事务尽量短) BEGIN IMMEDIATE; UPDATE tasks SET done = 1 WHERE id = 1; UPDATE tasks SET done = 1 WHERE id = 2; COMMIT; -- 出错就 ROLLBACK;
PRAGMA journal_mode=WAL; -- 库级,执行一次永久生效 PRAGMA busy_timeout=5000; -- 连接级,每个连接都要设 PRAGMA foreign_keys=ON; -- 连接级,默认 OFF! PRAGMA synchronous=NORMAL; PRAGMA integrity_check; -- 体检:返回 ok 即库文件完好 PRAGMA wal_checkpoint; -- 手动把 WAL 合并回主库 .tables -- 命令行点命令:列出所有表 .schema tasks -- 命令行点命令:看建表语句
# 在线备份(不锁库,推荐给正在运行的库): sqlite3 workbench.db ".backup 'backups/workbench-20261014.db'" # 导出为 SQL 文本(适合做版本化迁移/小库): sqlite3 workbench.db .dump > dump.sql # 从 SQL 文本恢复: sqlite3 restored.db < dump.sql
不要在运行中的服务上直接 cp 主库文件——WAL 模式下主库与 -wal 是分离的,只 cp 主库会丢最近的数据。要么走 §8 的 VACUUM INTO / .backup,要么先正常停服务再 cp 三件套(.db + .db-wal + .db-shm)。
这是本模块的核心。先讲为什么选 modernc,再给完整可运行代码。
| 维度 | modernc.org/sqlite v1.60.1 | mattn/go-sqlite3 v1.14.52 |
|---|---|---|
| CGO | 纯 Go,无 CGO | CGO,需要 gcc |
| 交叉编译 | GOOS=linux GOARCH=amd64 一把过 | Windows 本机交叉编译到 Linux 需要 Linux 工具链(或 zig cc),折腾 |
| 1GB 服务器 | 免 gcc,拷贝二进制即跑 | 需在服务器装 build-essential,占磁盘 |
| 性能上限 | 够用(工作台量级无感知) | 略高(贴近原生 C) |
| 本教程结论 | 一律选它(工作台 GORM 驱动 glebarez 也是包它) | 仅当你本机常驻 Linux、且要榨极限性能时再考虑 |
go.mod(依赖清单,锁版本照抄):
module sqlite-demo go 1.27 require ( modernc.org/sqlite v1.60.1 )
main.go(整文件可运行):
package main import ( "database/sql" "fmt" "log" "time" _ "modernc.org/sqlite" // 注册 driver name "sqlite" ) func main() { // DSN:打开文件;?_pragma=... 会在每个新连接上自动执行 PRAGMA dsn := "workbench.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(ON)&_pragma=synchronous(NORMAL)" db, err := sql.Open("sqlite", dsn) if err != nil { log.Fatal(err) } defer db.Close() // SQLite 下单写者语义下,连接池不要放大。 // MaxOpenConns=1 写最稳;读多可放到 4-8,但写仍排队。 db.SetMaxOpenConns(4) db.SetMaxIdleConns(2) db.SetConnMaxLifetime(time.Hour) // 建表 _, err = db.Exec(`CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL )`) if err != nil { log.Fatal(err) } // 插入 res, err := db.Exec( `INSERT INTO tasks(title, done, created_at) VALUES(?, ?, ?)`, "写周报", 0, time.Now().Format(time.RFC3339), ) if err != nil { log.Fatal(err) } id, _ := res.LastInsertId() fmt.Println("inserted id =", id) // 查询 var ( title string done int ) row := db.QueryRow(`SELECT title, done FROM tasks WHERE id = ?`, id) if err := row.Scan(&title, &done); err != nil { log.Fatal(err) } fmt.Println("row:", title, done) // 事务:BEGIN IMMEDIATE 显式拿写锁,避免 deferred 升级时的死锁 tx, err := db.Begin() if err != nil { log.Fatal(err) } _, err = tx.Exec(`UPDATE tasks SET done = 1 WHERE id = ?`, id) if err != nil { tx.Rollback() log.Fatal(err) } if err := tx.Commit(); err != nil { log.Fatal(err) } fmt.Println("tx committed") }
运行与交叉编译:
# Windows 本机: go mod tidy go run . # 交叉编译到 1GB Linux 服务器(纯 Go 驱动,一把过,不需要 gcc): set GOOS=linux set GOARCH=amd64 go build -o sqlite-demo-linux . # 然后把 sqlite-demo-linux 拷到服务器,chmod +x 直接跑
sql.Open 本身不建立连接,第一个 Query 才连;② DSN 里的 _pragma=... modernc 会在每个新连接上自动执行,这正好解决 3.4 说的"连接级 PRAGMA 每个连接都要设"的问题;③ SetMaxOpenConns 别贪大——SQLite 写是全局串行的,连接开多了反而在锁上排队;④ 写密集场景把 MaxOpenConns 设成 1,读多场景 4–8。工作台(模块 11)的 internal/db/db.go 就是这段代码的展开。这里给出"深度配置版":WAL + busy_timeout + 外键 + 自动迁移 + 事务。
require ( github.com/glebarez/sqlite v1.11.0 // 纯 Go GORM 驱动,内部即 modernc v1.60.1 gorm.io/gorm v1.31.2 // GORM v2 API 线 )
package db import ( "fmt" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "workbench/internal/model" // User / Task(见工作台契约 §5) ) // Open 按 DB_DRIVER 打开数据库;sqlite 分支即工作台默认。 // dbPath 来自环境变量 DB_PATH,例如 ./data/workbench.db func Open(driver, dbPath string) (*gorm.DB, error) { switch driver { case "sqlite", "": return openSQLite(dbPath) case "mysql": // 见 16 MySQL 模块:gorm.io/driver/mysql,同一结构 return nil, fmt.Errorf("mysql 分支见 16 模块") default: return nil, fmt.Errorf("unknown driver: %s", driver) } } func openSQLite(dbPath string) (*gorm.DB, error) { // DSN:用 _pragma 把连接级 PRAGMA 注入每个新连接。 dsn := fmt.Sprintf( "%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(ON)&_pragma=synchronous(NORMAL)", dbPath, ) gdb, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Warn), // 生产用 Warn,别开 Info 刷屏 }) if err != nil { return nil, err } // 拿到底层 *sql.DB 设连接池(SQLite 单写者,池要小) sqlDB, err := gdb.DB() if err != nil { return nil, err } sqlDB.SetMaxOpenConns(4) sqlDB.SetMaxIdleConns(2) sqlDB.SetConnMaxLifetime(time.Hour) // 自动迁移(工作台契约 §5 的 User / Task) if err := gdb.AutoMigrate(&model.User{}, &model.Task{}); err != nil { return nil, err } return gdb, nil } // WithTx 演示工作台事务写法:短事务、无 IO func WithTx(gdb *gorm.DB, fn func(tx *gorm.DB) error) error { return gdb.Transaction(fn) }
internal/db/db.go 的 SQLite 分支配到生产形态。DB_DRIVER=sqlite(或留空)走 SQLite;DB_DRIVER=mysql 切到 16 模块的 MySQL 分支——业务代码一行不改,这就是当初把驱动做成可切换开关的原因。1GB 内存的机器,数据库选型第一原则是"常驻内存越小越好"。按下表决策,不要拍脑袋。
| 你的场景 | 选谁 | 理由 |
|---|---|---|
| 工作台 / 个人后台 / CLI 工具,写 QPS < 几十 | SQLite | 零常驻内存,单文件备份,交叉编译免 gcc |
| 内部报表 / 内容站,读多写极少 | SQLite | WAL 读并发够用;读多还可以加 18 模块的 Redis 缓存 |
| 原型 / MVP 验证期 | SQLite | 零前置,先把业务跑通再说 |
| 客户网站,多用户同时写、需账号权限 | MySQL(16 模块) | SQLite 单写者扛不住并发写;1GB 机器 MySQL 要调小 buffer pool |
| 复杂查询 / JSONB / 合规审计 | PostgreSQL(17 模块) | SQLite 没有 PG 的类型与约束体系 |
| 缓存 / 限流计数 / 队列 | Redis(18 模块) | 那是内存加速层,不是 SQLite 的竞争者 |
1GB 服务器的物理账:MySQL 8 默认配置光缓冲池就想吃 400MB+,PG 类似;而 SQLite 没有常驻进程,你的 Go 二进制吃多少内存它就吃多少。在 1GB 机器上,这个差距就是"能跑"和"OOM"的差距。
服务运行中、WAL 未合并时,VACUUM INTO 会生成一个干净、一致、 checkpoint 过的新库文件,全程不锁你的业务库:
-- 在任意连接上执行,目标文件不能已存在 VACUUM INTO '/var/backups/workbench-2026-10-14.db'; -- Go 里就是一行 Exec: db.Exec(`VACUUM INTO ?`, "/var/backups/workbench-"+time.Now().Format("20060102")+".db")
#!/usr/bin/env bash
# /usr/local/bin/sqlite-backup.sh —— 工作台 SQLite 每日热备份
set -euo pipefail
DB=/opt/workbench/data/workbench.db
BAK_DIR=/var/backups/workbench
KEEP_DAYS=14
mkdir -p "$BAK_DIR"
STAMP=$(date +%Y%m%d-%H%M%S)
OUT="$BAK_DIR/workbench-$STAMP.db"
# 方式一:用 VACUUM INTO(需要 sqlite3 命令行;库在运行也安全)
sqlite3 "$DB" "VACUUM INTO '$OUT';"
# 备份完顺手校验完整性
sqlite3 "$OUT" "PRAGMA integrity_check;" | grep -q '^ok$'
# 清理超期备份
find "$BAK_DIR" -name 'workbench-*.db' -mtime "+$KEEP_DAYS" -delete
echo "backup ok: $OUT"
# crontab -e 加一行:每天凌晨 3:15 跑
15 3 * * * /usr/local/bin/sqlite-backup.sh >> /var/log/sqlite-backup.log 2>&1
PRAGMA integrity_check。mode=ro,或命令行 sqlite3 file:workbench.db?mode=ro——只读连接完全不参与写锁,适合挂一个独立报表进程。CREATE INDEX idx_tasks_done ON tasks(done);)。SQLite 是 B 树引擎,缺索引时全表扫描比 MySQL 还脆(单文件小库读盘快,但几十万行后照样卡)。卡住了先查这张表。报错原文照抄进表格第一列。
| 报错原文 | 原因 | 解决 |
|---|---|---|
database is locked / SQLITE_BUSY | 另一个写事务未提交,或连接池里连接没释放 | DSN 加 busy_timeout(5000);检查事务是否 Commit/Rollback;写密集把 MaxOpenConns 调小到 1 |
no such table: tasks | 库文件路径不对,或 AutoMigrate 没跑 | 确认 DB_PATH 指向的文件;启动时先跑 AutoMigrate;命令行 .tables 核对 |
table tasks already exists | 手动建表与 AutoMigrate 冲突,或重复执行 DDL | DDL 一律写 CREATE TABLE IF NOT EXISTS;以 GORM AutoMigrate 为准,不要手工建表 |
SQLITE_BUSY: database is locked(带超时) | busy_timeout 没生效,或 WAL 没 checkpoint | 确认每个连接都设了 PRAGMA busy_timeout=5000;跑 PRAGMA wal_checkpoint; |
CGO 交叉编译失败(mattn 场景):gcc: no such file or directory | 用了 mattn CGO 驱动,Windows 本机没 Linux gcc | 换 modernc/glebarez 纯驱动(本教程默认);或装 zig cc 工具链——不值得 |
sql: unknown driver "sqlite3" ... | 没 import 驱动包的 side-effect,或 driver name 写错 | modernc 用 import _ "modernc.org/sqlite",driver name 是 "sqlite"(不是 "sqlite3");mattn 才是 "sqlite3" |
disk I/O error | 磁盘满、目录不可写、或 WAL 文件被别的进程删了 | 查磁盘空间 df -h;确认进程对 .db 所在目录有写权限;不要手删运行中的 -wal/-shm |
database disk image is malformed | 库文件损坏(断电、磁盘故障、只拷了主库没拷 WAL) | PRAGMA integrity_check;;从最近备份恢复;平时用 VACUUM INTO/.backup 而不是裸 cp |
unable to open database file | 目录不存在 / 路径拼错 / 权限不足 | DB_PATH 的父目录要先 mkdir(如 ./data/);检查 systemd 服务的 WorkingDirectory 与用户权限 |
| 锁表导致写超时:请求挂住后报 context deadline exceeded | 长事务里夹了网络请求/外部调用,占着写锁不放 | 事务体里只做 DB 操作;外部调用移出事务;写事务必须短 |
| 外键没生效:删了父行子行还在 | SQLite 默认 foreign_keys=OFF,没开 | DSN 加 _pragma=foreign_keys(ON),且确认每个新连接都执行到 |
| WAL 文件巨大不回收(workbench.db-wal 几个 G) | 长读事务一直不结束,checkpoint 无法推进 | 关掉泄漏的长读连接;手动 PRAGMA wal_checkpoint(TRUNCATE); |
做完以下两项,本模块与 M3 清单里的"SQLite(WAL + 备份)集成达标"同时达成。
| √ | 验收项 | 怎么算过 |
|---|---|---|
| ☐ | 工作台 SQLite 深度配置跑通:用 §6 的 db.go 配置(WAL + busy_timeout=5000 + foreign_keys=ON + synchronous=NORMAL + 小连接池)启动工作台 | 启动后目录出现 workbench.db / -wal / -shm;登录、任务 CRUD 正常;PRAGMA foreign_keys; 返回 1 |
| ☐ | 一次 VACUUM INTO 热备份演练:服务运行中执行 VACUUM INTO,再对备份跑 integrity_check | 备份文件生成;PRAGMA integrity_check; 返回 ok;原服务无中断 |
| ☐ | 交叉编译验证:本机 GOOS=linux GOARCH=amd64 go build 一把过(无 gcc 报错),二进制拷到服务器能跑 | 服务器上 ./workbench 启动成功,/healthz 返回 ok |
过不了怎么办:先查 §9 报错表;再不行把"报错原文 + 复现步骤 + 相关 PRAGMA"按 08 §6.3 的调试纪律打包给 AI。本模块不要求你去翻官网——该讲的都在上面了。
| SQLite 版本 3.53.4(2026-07-24)与官方变更记录 | https://sqlite.org/changes.html |
| modernc.org/sqlite v1.60.1(纯 Go 驱动) | https://pkg.go.dev/modernc.org/sqlite |
| glebarez/sqlite v1.11.0(GORM 纯 Go 驱动) | https://pkg.go.dev/github.com/glebarez/sqlite |
| mattn/go-sqlite3 v1.14.52(CGO 备选) | https://github.com/mattn/go-sqlite3/releases |
| GORM v1.31.2(v2 API 线) | https://pkg.go.dev/gorm.io/gorm |
| PRAGMA journal_mode=WAL / busy_timeout / foreign_keys / synchronous 语义;VACUUM INTO 热备份;单写者语义 | https://sqlite.org/pragma.html · https://sqlite.org/wal.html · https://sqlite.org/lang_vacuum.html |
| 四数据库分工、工作台默认 DB_DRIVER=sqlite、版本纪律七规则:本系列 08 §8.1 / 09 §4 / 工作台契约 | file:///D:/02-Docs/金融知识/08-闭关总纲.html · file:///D:/02-Docs/金融知识/09-环境搭建与版本清单.html |
(编者整理:选型决策表、连接池参数、cron 备份脚本、报错对照表为基于官方文档与工作台契约的工程化整理,无外部独立来源。)