用 database/sql 而非 mattn/go-sqlite3 裸 api,导入驱动后 sql.open 初始化,建表加 if not exists,设 pragma journal_mode = wal,增删改用 exec 配 lastinsertid,单行查用 queryrow,批量插入用多值语句,时间存 unix 时间戳,事务用 defer 判断 nil 和提交状态确保回滚安全。

SQLite 在 Go 里用 database/sql 就够了,别碰 mattn/go-sqlite3 的裸 API
Go 官方 database/sql 接口抽象得足够好,SQLite 驱动(mattn/go-sqlite3)只是个后端实现。直接调用它的 sqlite3.Open 或手写 C. 相关逻辑,反而绕开连接池、事务封装和上下文取消支持,后续加并发或超时会踩坑。
常见错误现象:panic: runtime error: invalid memory address,往往源于手动管理 *sqlite3.SQLiteConn 后忘了 Close,或在多 goroutine 中复用非线程安全的裸连接。
- 初始化只做两件事:导入驱动
_ "github.com/mattn/go-sqlite3",再用sql.Open("sqlite3", "contacts.db") - 建表语句必须带
IF NOT EXISTS,否则每次重启程序都可能报table contacts already exists -
PRAGMA journal_mode = WAL建议在Open后立刻执行一次,提升并发读写性能,但注意它不支持内存数据库(:memory:)
Exec 和 QueryRow 别混用:增删改用 Exec,单行查用 QueryRow
新手常把 INSERT 之后接 QueryRow,以为能拿到自增 ID —— 实际上 QueryRow 会尝试解析结果集,而 INSERT 没有返回列,导致 Scan 报 sql: no rows in result set 或静默失败。
正确姿势是:用 Exec 执行增删改,再调 Result.LastInsertId() 拿 ID;单行查询(如按 ID 查联系人)才用 QueryRow。
立即学习“go语言免费学习笔记(深入)”;
-
INSERT INTO contacts (name, phone) VALUES (?, ?)→db.Exec(...)→lastID, _ := res.LastInsertId() -
SELECT name, phone FROM contacts WHERE id = ?→db.QueryRow(...).Scan(&name, &phone) - 批量插入别用循环多次
Exec,改用参数化多值语句:INSERT INTO contacts VALUES (?,?),(?,?),(?,?),最多一次塞 500 行,避免参数超限(SQLite 默认 999)
时间字段存 INTEGER(Unix 时间戳),别存 TEXT 或 DATETIME
SQLite 本身没有原生日期类型,DATETIME 是文本模拟,排序、范围查询(如“最近 7 天”)会出错:字符串比较 "2024-10-01" "2024-9-30" 成立,但逻辑上不成立。
存 INTEGER 用 time.Now().Unix(),查的时候转回 time.Unix(ts, 0),简单可靠。
- 建表写
created_at INTEGER NOT NULL,不是DATETIME DEFAULT CURRENT_TIMESTAMP - WHERE 条件写
created_at >= ?,参数传sevenDaysAgo.Unix(),别拼字符串 - 如果必须导出可读时间,用
strftime('%Y-%m-%d %H:%M', created_at, 'unixepoch')在 SQL 层转换,而不是在 Go 里反复time.Unix(...).Format(...)
事务没 Commit 就退出?用 defer tx.Rollback() + 显式 Commit 配合
Go 的 defer 看似方便,但写成 defer tx.Rollback() 然后在成功路径里忘掉 tx.Commit(),会导致每次操作都回滚 —— 表面无报错,数据就是不落盘,极难排查。
真正安全的写法是:先 defer func() 匿名函数,在里面判断 tx 是否为 nil 且未提交,再决定是否回滚。
- 标准模板:
tx, err := db.Begin() if err != nil { return err } defer func() { if tx != nil { tx.Rollback() } }() // ... 操作 if err := tx.Commit(); err == nil { tx = nil // 标记已提交 } - 不要依赖
recover()捕获 panic 后自动回滚 —— SQLite 驱动不保证 panic 时连接状态干净 - 事务内避免调用外部 HTTP 或阻塞 I/O,超时会导致锁表,其他查询被卡住
最麻烦的从来不是语法,而是事务中途 panic 了但没触发 defer,或者 tx.Commit() 返回 error 却被忽略 —— 这时候日志里只有 database is locked,得翻调用栈看哪条语句没 finish。










