Go SQLite 快速入门指南:从零开始掌握嵌入式数据库操作
引言
在现代软件开发中,数据持久化是不可或缺的一环。关系型数据库因其结构化、事务支持和强大的查询能力而备受青睐。SQLite 是一个非常独特且广泛使用的关系型数据库管理系统(RDBMS)。与传统的客户端-服务器(Client-Server)数据库(如 MySQL、PostgreSQL)不同,SQLite 是一个嵌入式数据库,它将整个数据库(包括定义、表、索引和数据本身)存储在一个单一的跨平台文件中。这使得 SQLite 具有零配置、无服务器、事务性强、轻量级且易于部署等显著优势。
Go 语言(Golang)以其简洁的语法、高效的并发处理能力、强大的标准库以及快速的编译速度,在后端开发、微服务、命令行工具等领域越来越受欢迎。当 Go 的高效与 SQLite 的便捷相结合时,便能为需要本地数据存储、中小型应用、原型开发或嵌入式系统的项目提供一个强大而灵活的解决方案。
本指南旨在为 Go 开发者提供一个全面而详细的 SQLite 快速入门教程。我们将从环境准备开始,逐步深入到数据库连接、表结构定义(DDL)、数据操作(CRUD:增、查、改、删)、事务处理以及一些最佳实践。无论您是 Go 新手还是有经验的开发者,希望本指南都能帮助您快速掌握在 Go 中使用 SQLite 的核心技能。
本指南将涵盖以下内容:
- SQLite 简介与优势:了解 SQLite 的特点及其适用场景。
- 环境准备:安装 Go 环境和必要的 SQLite 驱动。
- 选择 Go SQLite 驱动:介绍常用的驱动及其特点。
- 连接数据库:学习如何建立与 SQLite 数据库文件的连接。
- 执行 DDL 操作:创建和管理数据库表结构。
- 执行 DML 操作 (CRUD):
- 插入 (INSERT) 数据,包括使用预处理语句。
- 查询 (SELECT) 数据(单行和多行)。
- 更新 (UPDATE) 数据。
- 删除 (DELETE) 数据。
- 使用事务 (Transactions):保证数据操作的原子性和一致性。
- 错误处理:Go 中数据库操作的常见错误处理模式。
- 高级话题(简述):如处理 NULL 值、连接池等。
- 完整示例:整合所学知识的综合案例。
- 总结与后续:回顾要点并提供进一步学习的方向。
预计阅读和实践时间:约 45-60 分钟。
1. SQLite 简介与优势
在深入代码之前,让我们简要回顾一下 SQLite 的关键特性:
- 嵌入式 (Serverless):SQLite 不是一个独立的服务器进程。它是一个 C 语言库,直接链接到应用程序中。数据库操作通过函数调用完成,而不是通过网络发送消息给服务器。
- 零配置 (Zero-Configuration):使用 SQLite 无需复杂的安装或管理。创建一个新的数据库就像创建一个新文件一样简单。
- 单一文件数据库 (Single File):整个数据库(表、索引、视图、数据等)都存储在宿主主机上的一个普通文件中。这使得数据库的备份、复制和移动变得极其容易。
- 事务性 (Transactional):SQLite 完全支持 ACID(原子性、一致性、隔离性、持久性)事务,即使在系统崩溃或电源故障的情况下也能保证数据的完整性。
- 跨平台 (Cross-Platform):数据库文件格式是跨平台的,可以在 32 位和 64 位系统以及大端和小端架构之间自由移动。
- 标准 SQL 支持:SQLite 支持大部分 SQL92 标准,并添加了一些自己的扩展。
- 轻量级:核心库非常小巧,资源占用低。
适用场景:
- 嵌入式设备和物联网 (IoT):资源有限的环境。
- 应用程序文件格式:作为桌面应用(如浏览器、版本控制系统)的数据存储后端。
- 数据分析:处理中等规模数据集的便捷工具。
- 缓存:作为服务端应用程序的本地缓存。
- 开发和测试:为大型数据库应用提供快速、简单的开发和测试环境。
- 命令行工具:需要持久化少量结构化数据的工具。
2. 环境准备
在开始之前,请确保您已经具备以下条件:
- Go 环境:安装了 Go (建议 1.16 或更高版本)。您可以通过在终端运行
go version
来检查。如果未安装,请访问 Go 官方网站 下载并安装。 - C 编译器 (GCC/Clang):大多数流行的 Go SQLite 驱动(如
mattn/go-sqlite3
)依赖 CGO 来调用底层的 SQLite C 库。因此,您需要安装一个 C 编译器。- Linux (Debian/Ubuntu):
sudo apt-get update && sudo apt-get install build-essential
- Linux (Fedora/CentOS/RHEL):
sudo yum groupinstall "Development Tools"
- macOS: 安装 Xcode 或 Command Line Tools (
xcode-select --install
)。 - Windows: 安装 MinGW-w64 或 TDM-GCC。确保将
gcc
添加到系统 PATH 环境变量中。
- Linux (Debian/Ubuntu):
3. 选择 Go SQLite 驱动
Go 的 database/sql
包提供了一个通用的数据库操作接口。要与特定的数据库(如 SQLite)交互,我们需要一个实现了 database/sql/driver
接口的驱动程序。
对于 SQLite,最常用和维护最活跃的驱动是:
github.com/mattn/go-sqlite3
: 这是目前最流行、功能最全面的 Go SQLite 驱动。它底层使用了amalgamation
版本的 SQLite C 库,并通过 CGO 与 Go 代码交互。功能丰富,性能良好,社区支持广泛。本指南将主要使用这个驱动。
安装 mattn/go-sqlite3
驱动:
打开您的终端或命令行,导航到您的 Go 项目目录(如果还没有,可以创建一个),然后运行以下命令:
bash
go get github.com/mattn/go-sqlite3
这条命令会自动下载并安装驱动包及其依赖项。如果您的 C 编译器环境配置正确,安装过程应该会顺利完成。如果遇到 CGO 相关的错误,请回过头检查第 2 节中的 C 编译器安装步骤。
4. 连接数据库
在 Go 中与数据库交互的第一步是建立连接。database/sql
包提供了 sql.Open()
函数来完成这项工作。
sql.Open()
函数签名:
go
func Open(driverName, dataSourceName string) (*sql.DB, error)
driverName
: 注册驱动时使用的名称。对于mattn/go-sqlite3
,这个名称是"sqlite3"
。dataSourceName
(DSN): 数据源名称,用于指定连接数据库所需的信息。对于 SQLite,DSN 通常就是数据库文件的路径。如果文件不存在,SQLite 会自动创建它。
示例代码:
“`go
package main
import (
“database/sql”
“log”
“os”
_ "github.com/mattn/go-sqlite3" // 匿名导入驱动,执行其 init() 函数进行注册
)
func main() {
dbPath := “./mydatabase.db” // 数据库文件路径
// 如果文件已存在,先删除(仅为演示,实际应用中请谨慎)
os.Remove(dbPath)
// 打开数据库连接
// DSN 就是文件路径
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatalf("无法打开数据库连接: %v", err)
}
// 使用 defer 确保在 main 函数退出时关闭数据库连接
// 这对于释放资源非常重要
defer func() {
if err := db.Close(); err != nil {
log.Printf("关闭数据库连接时出错: %v", err)
} else {
log.Println("数据库连接已成功关闭")
}
}()
// 验证数据库连接是否成功 (可选但推荐)
// sql.Open() 实际上并不会立即建立连接,它只是验证参数并准备好 *sql.DB 对象。
// Ping() 会尝试建立一个实际连接。
err = db.Ping()
if err != nil {
log.Fatalf("无法连接到数据库: %v", err)
}
log.Println("成功连接到 SQLite 数据库!")
// 在这里可以执行后续的数据库操作...
// ...
}
“`
代码说明:
- 导入包:我们导入了
database/sql
、log
(用于日志记录)、os
(用于文件操作,这里仅为演示删除旧文件)以及 SQLite 驱动github.com/mattn/go-sqlite3
。 - 匿名导入
_
:驱动包使用_
进行匿名导入。这是 Go 的一种惯例,表示我们只需要执行该包的init()
函数(驱动会在此函数中调用sql.Register
将自己注册到database/sql
),而不需要直接使用包中的任何导出标识符。 sql.Open()
:指定驱动名称"sqlite3"
和数据库文件路径dbPath
来获取*sql.DB
对象。*sql.DB
代表一个数据库连接池,是线程安全的,可以长期持有。- 错误处理:每次数据库操作都可能返回错误,必须检查并处理
err
。这里使用log.Fatalf
在关键错误时终止程序。 defer db.Close()
:使用defer
语句确保在函数(这里是main
)结束时调用db.Close()
。这会释放连接池中的所有连接和相关资源。这是一个非常重要的实践。db.Ping()
:调用Ping()
尝试与数据库建立实际连接并验证 DSN 是否有效。这是确认连接是否真正可用的好方法。
运行这段代码,如果一切顺利,您将在当前目录下看到一个名为 mydatabase.db
的新文件,并在控制台看到成功连接的日志。
5. 执行 DDL 操作 (创建表)
DDL (Data Definition Language) 用于定义数据库结构,例如创建、修改或删除表。在 database/sql
中,通常使用 db.Exec()
方法来执行不返回结果集的 SQL 语句(如 CREATE TABLE
, ALTER TABLE
, DROP TABLE
)。
db.Exec()
函数签名:
go
func (db *DB) Exec(query string, args ...interface{}) (sql.Result, error)
query
: 要执行的 SQL 语句字符串。args
: 可选的参数,用于 SQL 语句中的占位符(稍后在 DML 中详细介绍)。对于简单的 DDL 语句,通常不需要参数。- 返回值:
sql.Result
接口(包含 LastInsertId 和 RowsAffected 等信息,对于 DDL 通常不关心)和error
。
示例:创建一个 users
表
“`go
package main
import (
“database/sql”
“log”
“os”
_ "github.com/mattn/go-sqlite3"
)
func main() {
dbPath := “./mydatabase.db”
os.Remove(dbPath) // 清理旧文件
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatalf("无法打开数据库: %v", err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatalf("无法连接数据库: %v", err)
}
log.Println("成功连接到 SQLite 数据库!")
// 定义 CREATE TABLE 语句
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);` // 使用反引号` `可以方便地编写多行 SQL
// 执行 SQL 语句
_, err = db.Exec(createTableSQL) // DDL 操作我们通常不关心 sql.Result
if err != nil {
log.Fatalf("创建 users 表失败: %v", err)
}
log.Println("成功创建 'users' 表!")
// 可以再创建一个表,例如 posts
createPostsTableSQL := `
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);`
_, err = db.Exec(createPostsTableSQL)
if err != nil {
log.Fatalf("创建 posts 表失败: %v", err)
}
log.Println("成功创建 'posts' 表!")
}
“`
代码说明:
- SQL 语句:我们使用多行字符串定义了
CREATE TABLE
语句,创建了一个users
表和一个posts
表。IF NOT EXISTS
:防止表已存在时报错。INTEGER PRIMARY KEY AUTOINCREMENT
:SQLite 中定义自增主键的标准方式。NOT NULL
,UNIQUE
:常见的列约束。DEFAULT CURRENT_TIMESTAMP
:设置默认值为当前时间戳。FOREIGN KEY
:在posts
表中定义了外键,关联到users
表的id
,并设置了ON DELETE CASCADE
,表示删除用户时,该用户的所有帖子也会被自动删除。
db.Exec()
:调用db.Exec()
执行创建表的 SQL。- 错误检查:每次
db.Exec()
调用后都检查错误。
运行此代码后,mydatabase.db
文件会被创建(或覆盖),并且内部会包含 users
和 posts
这两个表的结构。
6. 执行 DML 操作 (CRUD)
DML (Data Manipulation Language) 用于操作数据库中的数据,包括插入 (INSERT)、查询 (SELECT)、更新 (UPDATE) 和删除 (DELETE)。
6.1 插入数据 (INSERT)
插入数据通常也使用 db.Exec()
。然而,为了防止 SQL 注入攻击并提高性能(对于重复执行的语句),强烈建议使用 预处理语句 (Prepared Statements)。
SQL 注入风险(不推荐的方式):
go
// 错误示例:直接拼接字符串,容易受到 SQL 注入攻击
userName := "Alice"
userEmail := "[email protected]"
sqlStmt := fmt.Sprintf("INSERT INTO users (name, email) VALUES ('%s', '%s')", userName, userEmail)
_, err := db.Exec(sqlStmt) // 非常危险!
正确方式:使用参数化查询 (预处理语句)
database/sql
包会自动处理预处理语句的创建和缓存(在一定程度上)。你可以直接在 db.Exec()
或 db.Query()
等方法中使用 ?
作为占位符,并将实际的值作为参数传递。
示例:插入用户数据
“`go
package main
import (
“database/sql”
“log”
“time”
_ "github.com/mattn/go-sqlite3"
)
func main() {
// … (省略数据库连接和建表代码,假设已完成)
dbPath := “./mydatabase.db”
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil { log.Fatalf(“无法打开数据库: %v”, err) }
defer db.Close()
if err = db.Ping(); err != nil { log.Fatalf(“无法连接数据库: %v”, err) }
log.Println(“成功连接到 SQLite 数据库!”)
// 准备插入语句,使用 ? 作为占位符
insertSQL := "INSERT INTO users(name, email) VALUES (?, ?)"
// 待插入的数据
usersToInsert := []struct {
Name string
Email string
}{
{"Alice", "[email protected]"},
{"Bob", "[email protected]"},
{"Charlie", "[email protected]"},
}
for _, user := range usersToInsert {
// 执行插入操作,将实际值作为参数传递
result, err := db.Exec(insertSQL, user.Name, user.Email)
if err != nil {
log.Printf("插入用户 %s (%s) 失败: %v", user.Name, user.Email, err)
continue // 继续尝试插入下一个用户
}
// 获取最后插入的 ID (对于自增主键很有用)
lastID, err := result.LastInsertId()
if err != nil {
log.Printf("获取用户 %s 的 LastInsertId 失败: %v", user.Name, err)
} else {
log.Printf("成功插入用户: Name=%s, Email=%s, ID=%d", user.Name, user.Email, lastID)
}
// 获取受影响的行数 (对于 INSERT 通常是 1)
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf("获取用户 %s 的 RowsAffected 失败: %v", user.Name, err)
} else {
log.Printf("用户 %s 插入影响行数: %d", user.Name, rowsAffected)
}
}
// 也可以显式创建 Prepared Statement (适用于需要重复执行大量相同语句的场景)
stmt, err := db.Prepare("INSERT INTO posts(user_id, title, content) VALUES (?, ?, ?)")
if err != nil {
log.Fatalf("准备 posts 插入语句失败: %v", err)
}
// 使用 defer 确保 Statement 在不再需要时关闭
defer stmt.Close()
// 假设 Alice (ID=1) 发表了两篇文章
_, err = stmt.Exec(1, "Alice's First Post", "Content of the first post.")
if err != nil {
log.Printf("插入 Alice 的第一篇文章失败: %v", err)
} else {
log.Println("成功插入 Alice 的第一篇文章")
}
_, err = stmt.Exec(1, "Alice's Second Post", "Content of the second post.")
if err != nil {
log.Printf("插入 Alice 的第二篇文章失败: %v", err)
} else {
log.Println("成功插入 Alice 的第二篇文章")
}
// 假设 Bob (ID=2) 发表了一篇文章
_, err = stmt.Exec(2, "Bob's Post", "Content of Bob's post.")
if err != nil {
log.Printf("插入 Bob 的文章失败: %v", err)
} else {
log.Println("成功插入 Bob 的文章")
}
}
“`
代码说明:
- 占位符
?
:SQL 语句中使用?
作为参数的占位符。驱动程序会安全地将后续参数替换到这些位置,防止 SQL 注入。 db.Exec()
参数:将要插入的值按顺序传递给db.Exec()
。sql.Result
:db.Exec()
返回的sql.Result
包含了LastInsertId()
和RowsAffected()
方法,可以获取自增 ID 和受影响的行数。- 显式
db.Prepare()
:对于需要频繁执行的语句(例如在一个循环中插入大量数据),显式创建sql.Stmt
对象(通过db.Prepare
)可以获得更好的性能,因为它只需要解析和编译 SQL 语句一次。 stmt.Exec()
:使用准备好的stmt
对象执行插入,传入参数。defer stmt.Close()
:显式创建的sql.Stmt
对象也需要在使用完毕后关闭,以释放资源。
6.2 查询数据 (SELECT)
查询数据是数据库操作中最常见的任务。database/sql
提供了两种主要方法:
db.QueryRow()
:用于查询 最多只返回一行 的结果。如果查询没有返回行,Scan()
方法会返回sql.ErrNoRows
错误。db.Query()
:用于查询 可能返回多行 的结果。它返回一个*sql.Rows
对象,你需要迭代这个对象来获取每一行的数据。
6.2.1 查询单行数据 (db.QueryRow()
)
示例:根据 ID 查询用户信息
“`go
package main
import (
“database/sql”
“log”
“time” // 需要导入 time 包来接收 TIMESTAMP 列
_ "github.com/mattn/go-sqlite3"
)
// 定义一个 User 结构体来映射查询结果
type User struct {
ID int64
Name string
Email string
CreatedAt time.Time // 使用 time.Time 来接收 TIMESTAMP
}
func main() {
// … (省略数据库连接代码,假设已有数据)
dbPath := “./mydatabase.db”
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil { log.Fatalf(“无法打开数据库: %v”, err) }
defer db.Close()
if err = db.Ping(); err != nil { log.Fatalf(“无法连接数据库: %v”, err) }
log.Println(“成功连接到 SQLite 数据库!”)
querySQL := "SELECT id, name, email, created_at FROM users WHERE id = ?"
var userIDToFind int64 = 1 // 假设我们要查找 ID 为 1 的用户
var user User // 创建一个 User 变量来接收结果
// 执行查询,传入参数 userIDToFind
// QueryRow 返回一个 *sql.Row 对象
row := db.QueryRow(querySQL, userIDToFind)
// 使用 Scan 将查询结果的列映射到 user 结构体的字段
// Scan 参数的顺序必须与 SELECT 语句中列的顺序严格一致
err = row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
// 特别处理查询结果为空的情况
if err == sql.ErrNoRows {
log.Printf("未找到 ID 为 %d 的用户", userIDToFind)
} else {
// 处理其他可能的错误(如数据库错误、类型转换错误等)
log.Fatalf("查询用户 ID %d 失败: %v", userIDToFind, err)
}
} else {
log.Printf("找到用户: ID=%d, Name=%s, Email=%s, CreatedAt=%s",
user.ID, user.Name, user.Email, user.CreatedAt.Format(time.RFC3339))
}
// 尝试查询一个不存在的用户
userIDToFind = 999
row = db.QueryRow(querySQL, userIDToFind)
err = row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("未找到 ID 为 %d 的用户 (预期结果)", userIDToFind)
} else {
log.Fatalf("查询用户 ID %d 时发生意外错误: %v", userIDToFind, err)
}
} else {
log.Printf("意外地找到了用户: %+v", user) // 这不应该发生
}
}
“`
代码说明:
User
结构体:定义一个结构体来组织查询结果,提高代码可读性。注意CreatedAt
字段类型为time.Time
以匹配数据库的TIMESTAMP
。db.QueryRow()
:执行只期望返回一行的查询。它本身不返回错误,错误是在调用Scan()
时返回的。row.Scan()
:这是关键步骤。Scan()
将查询结果的每一列按顺序复制到传入的指针参数中。- 参数必须是指针 (
&user.ID
,&user.Name
, …)。 - 参数的数量和顺序必须与
SELECT
语句中的列完全匹配。 - Go 类型必须与数据库列类型兼容(
database/sql
会尝试进行一些自动转换)。
- 参数必须是指针 (
sql.ErrNoRows
:这是一个特殊的错误,表示查询成功执行但没有返回任何行。必须显式检查这个错误,以便区分“未找到”和“查询出错”两种情况。
6.2.2 查询多行数据 (db.Query()
)
示例:查询所有用户
“`go
package main
import (
“database/sql”
“log”
“time”
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int64
Name string
Email string
CreatedAt time.Time
}
func main() {
// … (省略数据库连接代码,假设已有数据)
dbPath := “./mydatabase.db”
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil { log.Fatalf(“无法打开数据库: %v”, err) }
defer db.Close()
if err = db.Ping(); err != nil { log.Fatalf(“无法连接数据库: %v”, err) }
log.Println(“成功连接到 SQLite 数据库!”)
querySQL := "SELECT id, name, email, created_at FROM users ORDER BY created_at DESC"
// 执行查询,可能返回多行
rows, err := db.Query(querySQL)
if err != nil {
log.Fatalf("查询所有用户失败: %v", err)
}
// !!! 非常重要:使用 defer 确保在函数结束时关闭 rows
// 这会释放底层的数据库连接回连接池
defer rows.Close()
var users []User // 创建一个切片来存储所有用户
// 迭代查询结果
for rows.Next() { // rows.Next() 准备下一行供 Scan() 读取
var user User // 为当前行创建一个新的 User 变量
// 使用 Scan 读取当前行的数据
err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
log.Printf("扫描用户行失败: %v", err)
// 根据需要决定是继续处理下一行还是中断
continue // 这里选择记录错误并继续
}
// 将成功扫描的用户添加到切片中
users = append(users, user)
}
// 循环结束后,检查在迭代过程中是否发生错误 (例如连接中断)
if err = rows.Err(); err != nil {
log.Fatalf("迭代用户结果时发生错误: %v", err)
}
// 打印查询到的所有用户
if len(users) == 0 {
log.Println("数据库中没有用户。")
} else {
log.Printf("查询到 %d 个用户:\n", len(users))
for _, u := range users {
log.Printf(" - ID: %d, Name: %s, Email: %s, CreatedAt: %s\n",
u.ID, u.Name, u.Email, u.CreatedAt.Format(time.RFC3339))
}
}
}
“`
代码说明:
db.Query()
:执行可能返回多行的查询,返回*sql.Rows
和error
。defer rows.Close()
:极其重要! 必须在rows
不再需要时调用rows.Close()
。最安全的方式是使用defer rows.Close()
紧跟在错误检查之后。这会释放与结果集关联的数据库连接,否则可能导致连接泄漏。rows.Next()
:在for
循环中使用。rows.Next()
准备下一行数据供Scan()
读取。当没有更多行或发生错误时,它返回false
。rows.Scan()
:在循环内部调用,用于读取当前行的数据,用法与QueryRow
中的Scan
类似。rows.Err()
:在for rows.Next()
循环结束后,必须 调用rows.Err()
来检查在迭代过程中是否发生了错误(rows.Next()
返回false
可能是因为迭代完成,也可能是因为出错)。
6.3 更新数据 (UPDATE)
更新数据与插入数据类似,通常使用 db.Exec()
并配合参数化查询。
示例:更新用户的邮箱地址
“`go
package main
import (
“database/sql”
“log”
_ "github.com/mattn/go-sqlite3"
)
func main() {
// … (省略数据库连接代码,假设已有数据)
dbPath := “./mydatabase.db”
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil { log.Fatalf(“无法打开数据库: %v”, err) }
defer db.Close()
if err = db.Ping(); err != nil { log.Fatalf(“无法连接数据库: %v”, err) }
log.Println(“成功连接到 SQLite 数据库!”)
updateSQL := "UPDATE users SET email = ? WHERE id = ?"
newEmail := "[email protected]"
userIDToUpdate := int64(2) // 假设更新 Bob (ID=2) 的邮箱
// 执行更新操作
result, err := db.Exec(updateSQL, newEmail, userIDToUpdate)
if err != nil {
log.Fatalf("更新用户 ID %d 的邮箱失败: %v", userIDToUpdate, err)
}
// 检查受影响的行数
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf("获取更新操作影响行数失败: %v", err)
} else {
if rowsAffected == 0 {
log.Printf("警告:更新操作未影响任何行。用户 ID %d 可能不存在。", userIDToUpdate)
} else {
log.Printf("成功更新用户 ID %d 的邮箱,影响行数: %d", userIDToUpdate, rowsAffected)
}
}
// 尝试更新一个不存在的用户
nonExistentUserID := int64(999)
result, err = db.Exec(updateSQL, "[email protected]", nonExistentUserID)
if err != nil {
log.Fatalf("更新用户 ID %d 的邮箱失败: %v", nonExistentUserID, err)
}
rowsAffected, _ = result.RowsAffected() // 忽略错误检查(演示目的)
log.Printf("尝试更新不存在的用户 ID %d,影响行数: %d (预期为 0)", nonExistentUserID, rowsAffected)
}
“`
代码说明:
UPDATE
语句:使用SET
指定要更新的列和新值,使用WHERE
子句指定要更新哪些行。占位符?
用于新值和WHERE
条件中的值。db.Exec()
:执行更新语句,传入参数。result.RowsAffected()
:检查更新操作实际影响了多少行。这对于确认更新是否成功(例如,WHERE
条件是否匹配到了行)很有用。
6.4 删除数据 (DELETE)
删除数据同样使用 db.Exec()
和参数化查询。
示例:根据 ID 删除用户
“`go
package main
import (
“database/sql”
“log”
_ "github.com/mattn/go-sqlite3"
)
func main() {
// … (省略数据库连接代码,假设已有数据)
dbPath := “./mydatabase.db”
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil { log.Fatalf(“无法打开数据库: %v”, err) }
defer db.Close()
if err = db.Ping(); err != nil { log.Fatalf(“无法连接数据库: %v”, err) }
log.Println(“成功连接到 SQLite 数据库!”)
deleteSQL := "DELETE FROM users WHERE id = ?"
userIDToDelete := int64(3) // 假设删除 Charlie (ID=3)
// 执行删除操作
result, err := db.Exec(deleteSQL, userIDToDelete)
if err != nil {
log.Fatalf("删除用户 ID %d 失败: %v", userIDToDelete, err)
}
// 检查受影响的行数
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf("获取删除操作影响行数失败: %v", err)
} else {
if rowsAffected == 0 {
log.Printf("警告:删除操作未影响任何行。用户 ID %d 可能不存在。", userIDToDelete)
} else {
log.Printf("成功删除用户 ID %d,影响行数: %d", userIDToDelete, rowsAffected)
// 如果设置了外键 CASCADE,相关联的 posts 也会被删除
}
}
}
“`
代码说明:
DELETE
语句:使用WHERE
子句指定要删除的行。db.Exec()
:执行删除语句。result.RowsAffected()
:检查实际删除了多少行。
7. 使用事务 (Transactions)
事务是一系列数据库操作,这些操作要么全部成功执行,要么在发生错误时全部回滚(撤销),以确保数据的原子性(Atomicity)和一致性(Consistency)。这对于需要多个步骤才能完成的逻辑操作(例如银行转账)至关重要。
database/sql
提供了对事务的原生支持:
db.Begin()
: 开始一个新的事务,返回一个*sql.Tx
对象。tx.Exec()
,tx.QueryRow()
,tx.Query()
,tx.Prepare()
,tx.Stmt()
: 在事务对象tx
上执行操作,而不是在db
上。这些方法的行为与db
上的对应方法类似,但所有操作都在同一个事务上下文中执行。tx.Commit()
: 如果事务中的所有操作都成功,调用Commit()
将更改永久保存到数据库。tx.Rollback()
: 如果在事务执行过程中发生任何错误,或者基于业务逻辑需要取消事务,调用Rollback()
会撤销自Begin()
以来该事务内所做的所有更改。
使用 defer
进行事务管理
一个常见的健壮模式是使用 defer
结合 Rollback()
来确保事务在函数退出时(无论是正常结束还是因为 panic 或错误)要么被提交,要么被回滚。
示例:在一个事务中插入用户和他们的帖子
“`go
package main
import (
“database/sql”
“errors” // 用于创建示例错误
“log”
_ "github.com/mattn/go-sqlite3"
)
func main() {
// … (省略数据库连接和建表代码)
dbPath := “./mydatabase.db”
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil { log.Fatalf(“无法打开数据库: %v”, err) }
defer db.Close()
if err = db.Ping(); err != nil { log.Fatalf(“无法连接数据库: %v”, err) }
log.Println(“成功连接到 SQLite 数据库!”)
// 尝试在一个事务中添加 David 和他的两篇文章
err = addUserAndPostsTransactional(db, "David", "[email protected]", []struct{ Title, Content string }{
{"David's Life", "About David..."},
{"Go Programming", "Learning Go is fun!"},
})
if err != nil {
log.Printf("添加 David 及其文章失败: %v", err)
} else {
log.Println("成功添加 David 及其文章 (事务提交)")
}
// 尝试添加 Eve,但在添加文章时模拟一个错误,触发回滚
err = addUserAndPostsTransactional(db, "Eve", "[email protected]", []struct{ Title, Content string }{
{"Eve's Story", "Once upon a time..."},
{"Error Post", "This should cause a rollback"}, // 模拟这个插入会失败
})
if err != nil {
log.Printf("添加 Eve 及其文章失败 (事务应已回滚): %v", err)
// 可以在这里查询数据库确认 Eve 是否存在,她不应该存在
var count int
checkErr := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", "[email protected]").Scan(&count)
if checkErr == nil && count == 0 {
log.Println("确认:用户 Eve 未插入数据库,事务回滚成功。")
} else {
log.Printf("确认失败:查询 Eve 时出错 (%v) 或用户存在 (count=%d)。", checkErr, count)
}
} else {
log.Println("意外:添加 Eve 的事务似乎提交了!")
}
}
// addUserAndPostsTransactional 在一个事务中添加用户和他们的帖子
func addUserAndPostsTransactional(db *sql.DB, name, email string, posts []struct{ Title, Content string }) (err error) {
// 1. 开始事务
tx, err := db.Begin()
if err != nil {
log.Printf(“开始事务失败: %v”, err)
return err // 直接返回错误,因为事务未开始,无需回滚
}
// 2. 使用 defer 处理事务结束(Commit 或 Rollback)
// 这个 defer 函数会在 addUserAndPostsTransactional 返回前执行
defer func() {
if p := recover(); p != nil {
// 如果函数内部发生 panic,回滚事务
log.Printf("发生 Panic,回滚事务: %v", p)
rbErr := tx.Rollback()
if rbErr != nil {
log.Printf("Panic 时回滚事务失败: %v", rbErr)
}
panic(p) // 重新抛出 panic
} else if err != nil {
// 如果函数返回时 err 不为 nil (即操作出错),回滚事务
log.Printf("操作出错,回滚事务: %v", err)
rbErr := tx.Rollback()
if rbErr != nil {
log.Printf("错误时回滚事务失败: %v", rbErr)
}
// 不需要修改外部函数的 err,因为它已经是非 nil 了
} else {
// 如果函数正常返回且 err 为 nil,提交事务
log.Println("操作成功,提交事务")
commitErr := tx.Commit()
if commitErr != nil {
log.Printf("提交事务失败: %v", commitErr)
// !! 关键:如果 Commit 失败,需要将错误赋给外部函数的命名返回值 err
err = commitErr
}
}
}() // 注意这里有 () 表示立即执行 defer 注册的函数
// 3. 在事务中执行操作
// 插入用户
insertUserSQL := "INSERT INTO users(name, email) VALUES (?, ?)"
result, err := tx.Exec(insertUserSQL, name, email)
if err != nil {
log.Printf("事务中插入用户 %s 失败: %v", name, err)
return err // 返回错误,defer 会处理 Rollback
}
userID, err := result.LastInsertId()
if err != nil {
log.Printf("事务中获取用户 %s 的 LastInsertId 失败: %v", name, err)
return err // 返回错误,defer 会处理 Rollback
}
log.Printf("事务中成功插入用户 %s, ID: %d", name, userID)
// 准备帖子插入语句 (在事务中使用 tx.Prepare)
insertPostSQL := "INSERT INTO posts(user_id, title, content) VALUES (?, ?, ?)"
stmt, err := tx.Prepare(insertPostSQL)
if err != nil {
log.Printf("事务中准备 posts 插入语句失败: %v", err)
return err // 返回错误,defer 会处理 Rollback
}
// Prepared statement 在事务结束时会自动关闭,但显式关闭是好习惯(如果需要在 defer 前使用)
// 这里我们不显式关闭,依赖事务结束
// defer stmt.Close() // 如果没有后续 return err 的可能,可以在这里 defer
// 插入帖子
for _, post := range posts {
// 模拟 Eve 的第二篇文章插入失败
if name == "Eve" && post.Title == "Error Post" {
err = errors.New("模拟插入帖子失败")
log.Printf("事务中模拟插入帖子 '%s' 失败 for user %s", post.Title, name)
stmt.Close() // 出现错误时最好关闭 stmt
return err // 返回错误,defer 会处理 Rollback
}
_, err = stmt.Exec(userID, post.Title, post.Content)
if err != nil {
log.Printf("事务中插入帖子 '%s' 失败 for user ID %d: %v", post.Title, userID, err)
stmt.Close() // 出现错误时最好关闭 stmt
return err // 返回错误,defer 会处理 Rollback
}
log.Printf("事务中成功插入帖子: '%s' for user ID %d", post.Title, userID)
}
stmt.Close() // 正常完成后关闭
// 4. 如果所有操作都成功,函数将正常返回,err 为 nil
// defer 中的逻辑会检测到 err == nil 并调用 tx.Commit()
log.Printf("用户 %s 及其所有帖子已在事务中准备好", name)
return nil // 表示成功,触发 Commit
}
“`
代码说明:
db.Begin()
:启动事务。- 命名返回值
err error
:函数签名func(...) (err error)
使用了命名返回值err
。这允许defer
函数能够检查和修改这个err
变量。 defer func() { ... }()
:这是一个关键的模式。- 它在函数开始时注册,但在函数返回前执行。
- Panic 处理:
recover()
捕获可能发生的 panic,并回滚事务。 - 错误处理:检查外部函数的返回值
err
。如果err != nil
,说明在事务操作中发生了错误,需要调用tx.Rollback()
。 - 成功处理:如果
err == nil
,说明所有操作成功,调用tx.Commit()
。 - Commit 错误处理:
tx.Commit()
本身也可能失败(例如数据库连接断开)。如果失败,必须将Commit()
返回的错误赋给外部函数的命名返回值err
,这样调用者才知道事务最终没有成功。
- 使用
tx
对象:在Begin()
和Commit
/Rollback
之间,所有的数据库操作(Exec
,QueryRow
,Query
,Prepare
)都应该通过tx
对象执行,而不是db
。 - 模拟错误:代码中模拟了插入 Eve 的第二篇文章时发生错误,这会使得
addUserAndPostsTransactional
函数返回一个非nil
的err
,触发defer
中的Rollback
逻辑。
这个事务模式确保了操作的原子性:要么用户 David 和他的所有帖子都被添加,要么在添加 Eve 时因为错误导致 Eve 和她的第一篇帖子都不会被添加到数据库中。
8. 错误处理
在 Go 中进行数据库操作时,健壮的错误处理至关重要。以下是一些关键点:
- 检查所有错误:
database/sql
包中的几乎所有操作都会返回一个error
。永远不要忽略错误返回值。 即使你认为某个操作不太可能失败,也要进行检查。 - 区分
sql.ErrNoRows
:如前所述,当使用QueryRow().Scan()
时,如果查询没有返回行,会返回sql.ErrNoRows
。务必将这种情况与真正的数据库错误区分开。 - 处理特定驱动错误:虽然
database/sql
提供通用接口,但底层驱动可能会返回更具体的错误类型或错误代码。有时可能需要类型断言或检查错误字符串来处理特定情况(例如唯一约束冲突)。mattn/go-sqlite3
驱动可能会返回sqlite3.Error
类型,可以检查其Code
或ExtendedCode
字段。
go
import "github.com/mattn/go-sqlite3"
// ...
_, err := db.Exec("INSERT INTO users(name, email) VALUES (?, ?)", "Alice", "[email protected]")
if err != nil {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) { // 使用 errors.As 进行类型断言
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique || sqliteErr.ExtendedCode == sqlite3.ErrConstraintPrimaryKey {
log.Printf("插入失败:违反唯一约束 (可能是 email 重复): %v", err)
// 可以进行相应的处理,例如提示用户更改邮箱
} else {
log.Printf("发生 SQLite 错误: Code=%d, ExtendedCode=%d, Error=%s", sqliteErr.Code, sqliteErr.ExtendedCode, sqliteErr.Error())
}
} else {
log.Printf("发生未知数据库错误: %v", err)
}
} - 日志记录:记录详细的错误信息(包括发生的上下文)对于调试非常有帮助。
- 资源释放:确保使用
defer
关闭*sql.DB
,*sql.Rows
,*sql.Stmt
对象,防止资源泄漏。
9. 高级话题(简述)
本指南主要关注基础入门,但了解一些进阶概念也很有帮助:
- 处理 NULL 值:数据库中的列可能为
NULL
。如果直接Scan
一个NULL
值到 Go 的基本类型(如string
,int
)会报错。database/sql
提供了sql.NullString
,sql.NullInt64
,sql.NullFloat64
,sql.NullBool
,sql.NullTime
等类型来处理可为 NULL 的列。
go
var content sql.NullString // 用于接收可能为 NULL 的 posts.content
err := row.Scan(&postID, &userID, &title, &content, &createdAt)
if err != nil { /* ... */ }
if content.Valid { // 检查值是否为 NULL
log.Printf("Content: %s", content.String) // 获取非 NULL 值
} else {
log.Println("Content is NULL")
} - 连接池:
*sql.DB
对象本身代表一个数据库连接池。它会自动管理连接的创建、复用和销毁。可以通过以下方法调整连接池的行为:db.SetMaxOpenConns(n int)
:设置池中最大打开连接数(包括活动和空闲)。db.SetMaxIdleConns(n int)
:设置池中最大空闲连接数。db.SetConnMaxLifetime(d time.Duration)
:设置连接可被复用的最大时间。db.SetConnMaxIdleTime(d time.Duration)
:设置连接在关闭前保持空闲的最大时间。
对于 SQLite 这种嵌入式数据库,连接池的概念略有不同,因为连接通常是到本地文件的。但database/sql
的接口是一致的,mattn/go-sqlite3
驱动会适当地处理这些设置(例如,控制并发访问同一个数据库文件的程度)。通常默认设置即可满足大部分 SQLite 应用场景。
- 数据库迁移 (Migrations):在应用开发过程中,数据库模式(表结构)经常需要变更。手动修改 SQL 脚本容易出错且难以管理版本。使用数据库迁移工具(如
golang-migrate/migrate
,pressly/goose
,sql-migrate
)可以帮助你用版本化的 Go 或 SQL 文件来管理数据库模式的演进和回滚。 - ORM (Object-Relational Mapper):像 GORM (
gorm.io/gorm
) 或 SQLBoiler (github.com/volatiletech/sqlboiler
) 这样的 ORM 库提供了更高层次的抽象,允许你用 Go 结构体和方法直接操作数据库,减少编写 SQL 的需要。它们通常会增加一些学习曲线和潜在的性能开销,但在大型项目中可以提高开发效率。对于简单的 SQLite 应用,直接使用database/sql
通常更直接、更轻量。
10. 完整示例
下面是一个将前面介绍的许多概念整合在一起的完整 Go 程序示例:
“`go
package main
import (
“database/sql”
“errors”
“fmt”
“log”
“os”
“time”
_ "github.com/mattn/go-sqlite3"
sqlite3 "github.com/mattn/go-sqlite3" // 显式导入以使用 sqlite3.Error
)
const dbPath = “./comprehensive_example.db”
type User struct {
ID int64
Name string
Email string
CreatedAt time.Time
}
type Post struct {
ID int64
UserID int64
Title string
Content sql.NullString // Content 可以为 NULL
CreatedAt time.Time
}
func main() {
log.Println(“— Go SQLite Comprehensive Example —“)
// --- 1. 清理并连接数据库 ---
os.Remove(dbPath) // 清理旧文件
db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on") // 开启外键约束支持
if err != nil {
log.Fatalf("无法打开数据库: %v", err)
}
defer db.Close()
if err = db.Ping(); err != nil {
log.Fatalf("无法连接数据库: %v", err)
}
log.Println("数据库连接成功")
// --- 2. 创建表结构 ---
if err = createSchema(db); err != nil {
log.Fatalf("创建表结构失败: %v", err)
}
log.Println("数据库表结构创建成功")
// --- 3. 插入数据 (包括使用事务和处理唯一约束) ---
log.Println("\n--- 插入数据 ---")
aliceID, err := insertUser(db, "Alice", "[email protected]")
if err != nil {
log.Printf("插入 Alice 失败: %v", err)
} else {
log.Printf("插入 Alice 成功, ID: %d", aliceID)
}
bobID, err := insertUser(db, "Bob", "[email protected]")
if err != nil {
log.Printf("插入 Bob 失败: %v", err)
} else {
log.Printf("插入 Bob 成功, ID: %d", bobID)
}
// 尝试插入重复 Email
_, err = insertUser(db, "Alice Twin", "[email protected]")
if err != nil {
log.Printf("尝试插入重复 Email (预期错误): %v", err)
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
log.Println(" (检测到唯一约束错误)")
}
}
// 使用事务插入用户及其帖子
charliePosts := []struct{ Title, Content string }{
{"Charlie's Thoughts", "Thinking..." },
{"SQLite with Go", "It's quite straightforward."},
}
err = insertUserAndPosts(db, "Charlie", "[email protected]", charliePosts)
if err != nil {
log.Printf("事务插入 Charlie 及其帖子失败: %v", err)
} else {
log.Println("事务插入 Charlie 及其帖子成功")
}
// 尝试失败的事务
evePosts := []struct{ Title, Content string }{
{"Eve's Plan", "World domination?"},
{"Error Trigger", "This post insertion will fail."}, // 模拟失败点
}
err = insertUserAndPosts(db, "Eve", "[email protected]", evePosts)
if err != nil {
log.Printf("事务插入 Eve 及其帖子失败 (预期,事务已回滚): %v", err)
}
// 插入 Alice 的一些帖子 (Content 可能为 NULL)
if aliceID > 0 {
err = insertPost(db, aliceID, "Alice's First", "Content here.")
if err == nil { log.Println("插入 Alice's First 成功")}
err = insertPost(db, aliceID, "Alice's Secret", "") // 插入空 Content (非 NULL)
if err == nil { log.Println("插入 Alice's Secret 成功")}
err = insertPostWithNullContent(db, aliceID, "Alice's Null Content") // 插入 NULL Content
if err == nil { log.Println("插入 Alice's Null Content 成功")}
}
// --- 4. 查询数据 ---
log.Println("\n--- 查询数据 ---")
// 查询单个用户
log.Println("查询 Bob (ID:", bobID, "):")
bob, err := getUserByID(db, bobID)
if err != nil {
log.Printf("查询 Bob 失败: %v", err)
} else {
printUser(bob)
}
// 查询不存在的用户
log.Println("查询 ID 999:")
_, err = getUserByID(db, 999)
if err != nil && errors.Is(err, sql.ErrNoRows) {
log.Println(" (用户未找到 - 预期结果)")
} else if err != nil {
log.Printf(" 查询失败: %v", err)
}
// 查询所有用户
log.Println("查询所有用户:")
users, err := getAllUsers(db)
if err != nil {
log.Printf("查询所有用户失败: %v", err)
} else {
for _, u := range users {
printUser(u)
}
}
// 查询某个用户的所有帖子 (包括 NULL content)
if aliceID > 0 {
log.Printf("查询 Alice (ID: %d) 的所有帖子:", aliceID)
posts, err := getPostsByUserID(db, aliceID)
if err != nil {
log.Printf(" 查询失败: %v", err)
} else {
for _, p := range posts {
printPost(p)
}
}
}
// --- 5. 更新数据 ---
log.Println("\n--- 更新数据 ---")
if bobID > 0 {
err = updateUserEmail(db, bobID, "[email protected]")
if err != nil {
log.Printf("更新 Bob 邮箱失败: %v", err)
} else {
log.Println("更新 Bob 邮箱成功.")
bobUpdated, _ := getUserByID(db, bobID) // 忽略错误检查
printUser(bobUpdated)
}
}
// --- 6. 删除数据 ---
log.Println("\n--- 删除数据 ---")
if bobID > 0 {
err = deleteUser(db, bobID)
if err != nil {
log.Printf("删除 Bob (ID: %d) 失败: %v", bobID, err)
} else {
log.Printf("删除 Bob (ID: %d) 成功.", bobID)
// 验证是否已删除
_, err = getUserByID(db, bobID)
if errors.Is(err, sql.ErrNoRows) {
log.Println(" (确认 Bob 已被删除)")
}
}
}
// 删除 Alice (ID=1) 会级联删除她的帖子
if aliceID > 0 {
log.Printf("删除 Alice (ID: %d) (预期级联删除其帖子):", aliceID)
err = deleteUser(db, aliceID)
if err != nil {
log.Printf(" 删除失败: %v", err)
} else {
log.Println(" 删除 Alice 成功.")
posts, err := getPostsByUserID(db, aliceID)
if err == nil && len(posts) == 0 {
log.Println(" (确认 Alice 的帖子已被级联删除)")
} else {
log.Printf(" 确认失败: err=%v, post count=%d", err, len(posts))
}
}
}
log.Println("\n--- 示例结束 ---")
}
// createSchema 创建数据库表
func createSchema(db *sql.DB) error {
userTableSQL := CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
_, err := db.Exec(userTableSQL)
if err != nil {
return fmt.Errorf(“创建 users 表失败: %w”, err)
}
postTableSQL := `
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT, -- 允许 NULL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);`
_, err = db.Exec(postTableSQL)
if err != nil {
return fmt.Errorf("创建 posts 表失败: %w", err)
}
return nil
}
// insertUser 插入单个用户并返回其 ID
func insertUser(db *sql.DB, name, email string) (int64, error) {
insertSQL := “INSERT INTO users(name, email) VALUES (?, ?)”
result, err := db.Exec(insertSQL, name, email)
if err != nil {
return 0, fmt.Errorf(“db.Exec 插入用户失败: %w”, err)
}
lastID, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf(“result.LastInsertId 失败: %w”, err)
}
return lastID, nil
}
// insertPost 插入单个帖子
func insertPost(db *sql.DB, userID int64, title, content string) error {
insertSQL := “INSERT INTO posts(user_id, title, content) VALUES (?, ?, ?)”
_, err := db.Exec(insertSQL, userID, title, content)
if err != nil {
return fmt.Errorf(“插入帖子 ‘%s’ 失败: %w”, title, err)
}
return nil
}
// insertPostWithNullContent 插入 content 为 NULL 的帖子
func insertPostWithNullContent(db *sql.DB, userID int64, title string) error {
insertSQL := “INSERT INTO posts(user_id, title, content) VALUES (?, ?, ?)”
// 传递 sql.NullString{} 的零值,或显式传递 nil
_, err := db.Exec(insertSQL, userID, title, nil) // 或者 sql.NullString{Valid: false}
if err != nil {
return fmt.Errorf(“插入 NULL content 帖子 ‘%s’ 失败: %w”, title, err)
}
return nil
}
// insertUserAndPosts 在事务中插入用户和帖子
func insertUserAndPosts(db *sql.DB, name, email string, posts []struct{ Title, Content string }) (err error) {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf(“开始事务失败: %w”, err)
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit() // 赋给命名返回值 err
if err != nil {
err = fmt.Errorf(“提交事务失败: %w”, err)
}
}
}()
// 插入用户
insertUserSQL := "INSERT INTO users(name, email) VALUES (?, ?)"
result, err := tx.Exec(insertUserSQL, name, email)
if err != nil {
err = fmt.Errorf("事务中插入用户失败: %w", err)
return err
}
userID, err := result.LastInsertId()
if err != nil {
err = fmt.Errorf("事务中获取 LastInsertId 失败: %w", err)
return err
}
// 准备并插入帖子
insertPostSQL := "INSERT INTO posts(user_id, title, content) VALUES (?, ?, ?)"
stmt, err := tx.Prepare(insertPostSQL)
if err != nil {
err = fmt.Errorf("事务中准备帖子插入语句失败: %w", err)
return err
}
defer stmt.Close() // 在事务作用域内 defer
for _, post := range posts {
// 模拟 Eve 的失败点
if name == "Eve" && post.Title == "Error Trigger" {
err = errors.New("模拟的帖子插入错误")
return err // 返回错误,触发回滚
}
_, err = stmt.Exec(userID, post.Title, post.Content)
if err != nil {
err = fmt.Errorf("事务中插入帖子 '%s' 失败: %w", post.Title, err)
return err
}
}
return nil // 成功,触发 Commit
}
// getUserByID 根据 ID 查询用户
func getUserByID(db *sql.DB, id int64) (User, error) {
querySQL := “SELECT id, name, email, created_at FROM users WHERE id = ?”
var user User
row := db.QueryRow(querySQL, id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, sql.ErrNoRows // 返回 ErrNoRows 以便上层区分
}
return User{}, fmt.Errorf(“扫描用户 ID %d 失败: %w”, id, err)
}
return user, nil
}
// getAllUsers 查询所有用户
func getAllUsers(db *sql.DB) ([]User, error) {
querySQL := “SELECT id, name, email, created_at FROM users ORDER BY id”
rows, err := db.Query(querySQL)
if err != nil {
return nil, fmt.Errorf(“查询所有用户失败: %w”, err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt); err != nil {
return nil, fmt.Errorf("扫描用户行失败: %w", err) // 如果一行扫描失败,通常应该返回错误
}
users = append(users, user)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("迭代用户结果时出错: %w", err)
}
return users, nil
}
// getPostsByUserID 查询某个用户的所有帖子
func getPostsByUserID(db *sql.DB, userID int64) ([]Post, error) {
querySQL := “SELECT id, user_id, title, content, created_at FROM posts WHERE user_id = ? ORDER BY created_at DESC”
rows, err := db.Query(querySQL, userID)
if err != nil {
return nil, fmt.Errorf(“查询用户 %d 的帖子失败: %w”, userID, err)
}
defer rows.Close()
var posts []Post
for rows.Next() {
var post Post
// 使用 sql.NullString 来扫描 content 列
if err := rows.Scan(&post.ID, &post.UserID, &post.Title, &post.Content, &post.CreatedAt); err != nil {
return nil, fmt.Errorf("扫描帖子行失败: %w", err)
}
posts = append(posts, post)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("迭代帖子结果时出错: %w", err)
}
return posts, nil
}
// updateUserEmail 更新用户邮箱
func updateUserEmail(db *sql.DB, id int64, newEmail string) error {
updateSQL := “UPDATE users SET email = ? WHERE id = ?”
result, err := db.Exec(updateSQL, newEmail, id)
if err != nil {
return fmt.Errorf(“更新用户 ID %d 邮箱失败: %w”, id, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
// 即使获取 RowsAffected 失败,更新可能已经成功,所以只记录警告
log.Printf(“警告: 获取更新用户 ID %d 的影响行数失败: %v”, id, err)
} else if rowsAffected == 0 {
return fmt.Errorf(“更新用户 ID %d 邮箱失败: 未找到该用户”, id) // 或者返回一个特定的错误类型
}
return nil
}
// deleteUser 删除用户
func deleteUser(db *sql.DB, id int64) error {
deleteSQL := “DELETE FROM users WHERE id = ?”
result, err := db.Exec(deleteSQL, id)
if err != nil {
return fmt.Errorf(“删除用户 ID %d 失败: %w”, id, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf(“警告: 获取删除用户 ID %d 的影响行数失败: %v”, id, err)
} else if rowsAffected == 0 {
return fmt.Errorf(“删除用户 ID %d 失败: 未找到该用户”, id)
}
// ON DELETE CASCADE 会自动处理 posts 表
return nil
}
// printUser 辅助函数,打印用户信息
func printUser(user User) {
log.Printf(” User – ID: %d, Name: %s, Email: %s, CreatedAt: %s”,
user.ID, user.Name, user.Email, user.CreatedAt.Format(time.RFC3339))
}
// printPost 辅助函数,打印帖子信息 (处理 NULL content)
func printPost(post Post) {
contentStr := “NULL”
if post.Content.Valid {
contentStr = fmt.Sprintf(“‘%s'”, post.Content.String) // 加引号以便区分空字符串和 NULL
}
log.Printf(” Post – ID: %d, UserID: %d, Title: ‘%s’, Content: %s, CreatedAt: %s”,
post.ID, post.UserID, post.Title, contentStr, post.CreatedAt.Format(time.RFC3339))
}
“`
这个示例展示了:
- 使用 DSN 参数
?_foreign_keys=on
启用外键约束。 - 封装数据库操作到独立的函数中,提高代码模块化和可重用性。
- 使用
fmt.Errorf
和%w
来包装错误,保留原始错误信息。 - 处理
sql.ErrNoRows
并将其传递给调用者。 - 处理唯一约束冲突错误。
- 在事务中封装多个操作,并实现可靠的回滚/提交逻辑。
- 使用
sql.NullString
处理可能为 NULL 的content
列。 - 演示
ON DELETE CASCADE
外键约束的效果。 - 使用辅助函数打印结构化输出。
11. 总结与后续
通过本指南,我们详细介绍了如何在 Go 语言中使用 database/sql
包和 mattn/go-sqlite3
驱动与 SQLite 数据库进行交互。我们涵盖了从基本的环境设置、数据库连接,到核心的 CRUD 操作(创建表、插入、查询、更新、删除),再到保证数据一致性的事务处理,以及重要的错误处理和资源管理实践。我们还简要提及了处理 NULL 值、连接池、数据库迁移和 ORM 等进阶主题。
关键要点回顾:
- SQLite 是一个轻量级、嵌入式、零配置的单文件数据库,非常适合 Go 应用中的本地存储场景。
- 使用
database/sql
标准接口和特定数据库驱动(如mattn/go-sqlite3
)。 - 始终使用参数化查询(占位符
?
)来防止 SQL 注入。 db.Exec()
用于执行不返回行的语句 (INSERT, UPDATE, DELETE, CREATE TABLE)。db.QueryRow()
用于查询单行,db.Query()
用于查询多行。Scan()
方法用于将查询结果映射到 Go 变量,注意参数顺序和指针。- 必须 检查所有数据库操作返回的
error
。 - 特别处理
sql.ErrNoRows
错误。 - 必须 使用
defer
关闭*sql.DB
,*sql.Rows
,*sql.Stmt
以释放资源。 - 使用事务 (
db.Begin
,tx.Commit
,tx.Rollback
) 保证复杂操作的原子性,并使用defer
实现健壮的事务管理。 - 使用
sql.Null*
类型处理数据库中的 NULL 值。
Go 与 SQLite 的结合提供了一个强大而高效的解决方案。SQLite 的简单性使得快速原型开发和部署变得容易,而 Go 的性能和并发特性则能确保应用的响应能力。
下一步可以探索:
- 深入研究
mattn/go-sqlite3
驱动的特定功能和配置选项。 - 学习和实践数据库迁移工具(如
golang-migrate/migrate
)来管理数据库模式的演变。 - 根据项目复杂度,评估是否需要引入 ORM 库(如 GORM)。
- 探索 SQLite 的高级特性,如全文搜索 (FTS)、JSON 支持、窗口函数等,并了解如何在 Go 中使用它们。
- 将 SQLite 集成到 Web 应用(如使用
net/http
或 Gin/Echo 框架)或命令行工具(如使用 Cobra/Viper 库)中。
希望本指南为您在 Go 项目中使用 SQLite 打下了坚实的基础。祝您编码愉快!