Go SQLite 快速入门指南 – wiki基地


Go SQLite 快速入门指南:从零开始掌握嵌入式数据库操作

引言

在现代软件开发中,数据持久化是不可或缺的一环。关系型数据库因其结构化、事务支持和强大的查询能力而备受青睐。SQLite 是一个非常独特且广泛使用的关系型数据库管理系统(RDBMS)。与传统的客户端-服务器(Client-Server)数据库(如 MySQL、PostgreSQL)不同,SQLite 是一个嵌入式数据库,它将整个数据库(包括定义、表、索引和数据本身)存储在一个单一的跨平台文件中。这使得 SQLite 具有零配置、无服务器、事务性强、轻量级且易于部署等显著优势。

Go 语言(Golang)以其简洁的语法、高效的并发处理能力、强大的标准库以及快速的编译速度,在后端开发、微服务、命令行工具等领域越来越受欢迎。当 Go 的高效与 SQLite 的便捷相结合时,便能为需要本地数据存储、中小型应用、原型开发或嵌入式系统的项目提供一个强大而灵活的解决方案。

本指南旨在为 Go 开发者提供一个全面而详细的 SQLite 快速入门教程。我们将从环境准备开始,逐步深入到数据库连接、表结构定义(DDL)、数据操作(CRUD:增、查、改、删)、事务处理以及一些最佳实践。无论您是 Go 新手还是有经验的开发者,希望本指南都能帮助您快速掌握在 Go 中使用 SQLite 的核心技能。

本指南将涵盖以下内容:

  1. SQLite 简介与优势:了解 SQLite 的特点及其适用场景。
  2. 环境准备:安装 Go 环境和必要的 SQLite 驱动。
  3. 选择 Go SQLite 驱动:介绍常用的驱动及其特点。
  4. 连接数据库:学习如何建立与 SQLite 数据库文件的连接。
  5. 执行 DDL 操作:创建和管理数据库表结构。
  6. 执行 DML 操作 (CRUD)
    • 插入 (INSERT) 数据,包括使用预处理语句。
    • 查询 (SELECT) 数据(单行和多行)。
    • 更新 (UPDATE) 数据。
    • 删除 (DELETE) 数据。
  7. 使用事务 (Transactions):保证数据操作的原子性和一致性。
  8. 错误处理:Go 中数据库操作的常见错误处理模式。
  9. 高级话题(简述):如处理 NULL 值、连接池等。
  10. 完整示例:整合所学知识的综合案例。
  11. 总结与后续:回顾要点并提供进一步学习的方向。

预计阅读和实践时间:约 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 环境变量中。

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 数据库!")

// 在这里可以执行后续的数据库操作...
// ...

}
“`

代码说明:

  1. 导入包:我们导入了 database/sqllog(用于日志记录)、os(用于文件操作,这里仅为演示删除旧文件)以及 SQLite 驱动 github.com/mattn/go-sqlite3
  2. 匿名导入 _:驱动包使用 _ 进行匿名导入。这是 Go 的一种惯例,表示我们只需要执行该包的 init() 函数(驱动会在此函数中调用 sql.Register 将自己注册到 database/sql),而不需要直接使用包中的任何导出标识符。
  3. sql.Open():指定驱动名称 "sqlite3" 和数据库文件路径 dbPath 来获取 *sql.DB 对象。*sql.DB 代表一个数据库连接池,是线程安全的,可以长期持有。
  4. 错误处理:每次数据库操作都可能返回错误,必须检查并处理 err。这里使用 log.Fatalf 在关键错误时终止程序。
  5. defer db.Close():使用 defer 语句确保在函数(这里是 main)结束时调用 db.Close()。这会释放连接池中的所有连接和相关资源。这是一个非常重要的实践。
  6. 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' 表!")

}

“`

代码说明:

  1. 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,表示删除用户时,该用户的所有帖子也会被自动删除。
  2. db.Exec():调用 db.Exec() 执行创建表的 SQL。
  3. 错误检查:每次 db.Exec() 调用后都检查错误。

运行此代码后,mydatabase.db 文件会被创建(或覆盖),并且内部会包含 usersposts 这两个表的结构。


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 的文章")
}

}
“`

代码说明:

  1. 占位符 ?:SQL 语句中使用 ? 作为参数的占位符。驱动程序会安全地将后续参数替换到这些位置,防止 SQL 注入。
  2. db.Exec() 参数:将要插入的值按顺序传递给 db.Exec()
  3. sql.Resultdb.Exec() 返回的 sql.Result 包含了 LastInsertId()RowsAffected() 方法,可以获取自增 ID 和受影响的行数。
  4. 显式 db.Prepare():对于需要频繁执行的语句(例如在一个循环中插入大量数据),显式创建 sql.Stmt 对象(通过 db.Prepare)可以获得更好的性能,因为它只需要解析和编译 SQL 语句一次。
  5. stmt.Exec():使用准备好的 stmt 对象执行插入,传入参数。
  6. 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) // 这不应该发生
}

}
“`

代码说明:

  1. User 结构体:定义一个结构体来组织查询结果,提高代码可读性。注意 CreatedAt 字段类型为 time.Time 以匹配数据库的 TIMESTAMP
  2. db.QueryRow():执行只期望返回一行的查询。它本身不返回错误,错误是在调用 Scan() 时返回的。
  3. row.Scan():这是关键步骤。Scan() 将查询结果的每一列按顺序复制到传入的指针参数中。
    • 参数必须是指针 (&user.ID, &user.Name, …)。
    • 参数的数量和顺序必须与 SELECT 语句中的列完全匹配。
    • Go 类型必须与数据库列类型兼容(database/sql 会尝试进行一些自动转换)。
  4. 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))
    }
}

}
“`

代码说明:

  1. db.Query():执行可能返回多行的查询,返回 *sql.Rowserror
  2. defer rows.Close()极其重要! 必须在 rows 不再需要时调用 rows.Close()。最安全的方式是使用 defer rows.Close() 紧跟在错误检查之后。这会释放与结果集关联的数据库连接,否则可能导致连接泄漏。
  3. rows.Next():在 for 循环中使用。rows.Next() 准备下一行数据供 Scan() 读取。当没有更多行或发生错误时,它返回 false
  4. rows.Scan():在循环内部调用,用于读取当前行的数据,用法与 QueryRow 中的 Scan 类似。
  5. 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)

}
“`

代码说明:

  1. UPDATE 语句:使用 SET 指定要更新的列和新值,使用 WHERE 子句指定要更新哪些行。占位符 ? 用于新值和 WHERE 条件中的值。
  2. db.Exec():执行更新语句,传入参数。
  3. 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 也会被删除
    }
}

}
“`

代码说明:

  1. DELETE 语句:使用 WHERE 子句指定要删除的行。
  2. db.Exec():执行删除语句。
  3. result.RowsAffected():检查实际删除了多少行。

7. 使用事务 (Transactions)

事务是一系列数据库操作,这些操作要么全部成功执行,要么在发生错误时全部回滚(撤销),以确保数据的原子性(Atomicity)和一致性(Consistency)。这对于需要多个步骤才能完成的逻辑操作(例如银行转账)至关重要。

database/sql 提供了对事务的原生支持:

  1. db.Begin(): 开始一个新的事务,返回一个 *sql.Tx 对象。
  2. tx.Exec(), tx.QueryRow(), tx.Query(), tx.Prepare(), tx.Stmt(): 在事务对象 tx 上执行操作,而不是在 db 上。这些方法的行为与 db 上的对应方法类似,但所有操作都在同一个事务上下文中执行。
  3. tx.Commit(): 如果事务中的所有操作都成功,调用 Commit() 将更改永久保存到数据库。
  4. 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

}
“`

代码说明:

  1. db.Begin():启动事务。
  2. 命名返回值 err error:函数签名 func(...) (err error) 使用了命名返回值 err。这允许 defer 函数能够检查和修改这个 err 变量。
  3. defer func() { ... }():这是一个关键的模式。
    • 它在函数开始时注册,但在函数返回前执行。
    • Panic 处理recover() 捕获可能发生的 panic,并回滚事务。
    • 错误处理:检查外部函数的返回值 err。如果 err != nil,说明在事务操作中发生了错误,需要调用 tx.Rollback()
    • 成功处理:如果 err == nil,说明所有操作成功,调用 tx.Commit()
    • Commit 错误处理tx.Commit() 本身也可能失败(例如数据库连接断开)。如果失败,必须将 Commit() 返回的错误赋给外部函数的命名返回值 err,这样调用者才知道事务最终没有成功。
  4. 使用 tx 对象:在 Begin()Commit/Rollback 之间,所有的数据库操作(Exec, QueryRow, Query, Prepare)都应该通过 tx 对象执行,而不是 db
  5. 模拟错误:代码中模拟了插入 Eve 的第二篇文章时发生错误,这会使得 addUserAndPostsTransactional 函数返回一个非 nilerr,触发 defer 中的 Rollback 逻辑。

这个事务模式确保了操作的原子性:要么用户 David 和他的所有帖子都被添加,要么在添加 Eve 时因为错误导致 Eve 和她的第一篇帖子都不会被添加到数据库中。


8. 错误处理

在 Go 中进行数据库操作时,健壮的错误处理至关重要。以下是一些关键点:

  • 检查所有错误database/sql 包中的几乎所有操作都会返回一个 error永远不要忽略错误返回值。 即使你认为某个操作不太可能失败,也要进行检查。
  • 区分 sql.ErrNoRows:如前所述,当使用 QueryRow().Scan() 时,如果查询没有返回行,会返回 sql.ErrNoRows。务必将这种情况与真正的数据库错误区分开。
  • 处理特定驱动错误:虽然 database/sql 提供通用接口,但底层驱动可能会返回更具体的错误类型或错误代码。有时可能需要类型断言或检查错误字符串来处理特定情况(例如唯一约束冲突)。mattn/go-sqlite3 驱动可能会返回 sqlite3.Error 类型,可以检查其 CodeExtendedCode 字段。
    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 打下了坚实的基础。祝您编码愉快!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部