Go SQLite 数据库入门教程 – wiki基地


Go 语言 SQLite 数据库入门教程:从零开始掌握数据持久化

Go 语言以其简洁、高效和强大的并发能力,在构建各种应用,特别是后端服务和命令行工具方面越来越受欢迎。在许多这类应用中,数据持久化是一个核心需求。虽然大型项目可能需要 PostgreSQL、MySQL 等强大的客户端-服务器数据库,但对于轻量级应用、嵌入式系统、桌面应用、测试或者只需要一个简单、零配置的数据库时,SQLite 是一个极好的选择。

SQLite 是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它不需要单独的服务器进程,数据直接存储在应用程序可访问的文件中。这使得它非常适合 Go 应用程序,可以方便地集成到单个可执行文件中。

本篇文章将带你从零开始,详细学习如何在 Go 语言中使用标准的 database/sql 包以及常用的 SQLite 驱动程序,进行数据库的连接、创建表、增删改查(CRUD)等核心操作。我们将深入探讨预处理语句、事务、错误处理和上下文的使用,帮助你写出健壮、安全和高效的 Go 数据库代码。

1. 前置条件

在开始之前,你需要确保:

  1. 已安装 Go 语言环境: 访问 https://go.dev/dl/ 下载并安装最新版本的 Go。
  2. 基本的 Go 语言知识: 了解 Go 的基本语法、包管理和错误处理机制。
  3. 基本的 SQL 知识 (可选): 熟悉 SQL 的 CREATE TABLE, INSERT, SELECT, UPDATE, DELETE 语句会有帮助,但教程中也会解释相关概念。
  4. 一个代码编辑器或 IDE: 例如 VS Code, GoLand 等。

2. 选择并安装 SQLite 驱动

Go 语言通过标准的 database/sql 包提供了一套统一的数据库接口。要连接特定的数据库,你需要安装对应的数据库驱动程序。对于 SQLite,最常用的驱动是 mattn/go-sqlite3

打开你的终端,进入你的 Go 项目目录,执行以下命令安装驱动:

bash
go get github.com/mattn/go-sqlite3

这个命令会下载 go-sqlite3 包及其依赖,并将其添加到你的 go.mod 文件中。

安装完成后,你就可以在你的 Go 代码中导入并使用它了。请注意,虽然你需要 import 这个包,但在大多数情况下,你只需要通过空白导入 _ "github.com/mattn/go-sqlite3" 来注册驱动,而不需要直接调用它里面的函数(除了特殊配置)。

go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // 注册 SQLite 驱动
"log"
// 其他需要的包,如 fmt, os 等
)

空白导入 (_) 的作用是执行导入包的 init() 函数。go-sqlite3 驱动的 init() 函数会调用 database/sql 包的 sql.Register() 方法,将自身注册为 “sqlite3” 驱动。之后,你就可以通过这个名称来打开数据库连接了。

3. 连接或创建 SQLite 数据库文件

SQLite 数据库存储在一个文件中。连接数据库实际上就是打开或创建一个 .db 文件。使用 database/sql 包的 sql.Open() 函数来完成这个任务。

sql.Open() 函数接受两个参数:
1. driverName: 注册的驱动名称,对于 mattn/go-sqlite3 来说是 "sqlite3"
2. dataSourceName: 数据库的连接字符串(DSN)。对于 SQLite,这通常就是数据库文件的路径。如果文件不存在,它会被创建。你也可以使用 :memory: 作为 DSN 来创建一个内存数据库(数据不会持久化到文件)。

以下是如何连接或创建一个名为 example.db 的数据库文件:

“`go
package main

import (
“database/sql”
“fmt”
_ “github.com/mattn/go-sqlite3” // 注册 SQLite 驱动
“log”
“os”
)

func main() {
// 指定数据库文件路径
dbFile := “./example.db”

// 删除已存在的数据库文件 (仅为方便演示,实际应用中通常不需要)
// os.Remove(dbFile)

// 打开数据库连接
// sql.Open 不会立即建立连接,它只是验证参数并返回一个 DB 对象
db, err := sql.Open("sqlite3", dbFile)
if err != nil {
    log.Fatal("Error opening database:", err)
}
// 确保在 main 函数退出前关闭数据库连接
defer db.Close()

// 使用 Ping() 方法检查数据库连接是否有效
err = db.Ping()
if err != nil {
    log.Fatal("Error connecting to database:", err)
}

fmt.Println("Successfully connected to database:", dbFile)

// 后续数据库操作将在这里进行

}
“`

重要说明:

  • sql.Open() 函数不会立即建立到底层数据存储的连接。它只初始化一个 sql.DB 对象,该对象代表着数据库的抽象概念。只有当你执行第一次数据库操作(如 Ping(), Exec(), Query() 等)时,才会真正建立连接。
  • db.Close() 应该使用 defer 关键字来确保即使在发生错误时也能关闭连接,释放资源。
  • db.Ping() 是一个验证连接是否有效的好方法。

现在,运行你的 Go 程序,如果一切顺利,它会打印出连接成功的消息,并在当前目录下创建一个 example.db 文件(如果它不存在的话)。

4. 创建数据表 (CREATE TABLE)

有了数据库连接后,下一步通常是创建存储数据所需的表。我们使用 sql.DB 对象的 Exec() 方法来执行不返回结果集的 SQL 语句,比如 CREATE TABLE, INSERT, UPDATE, DELETE

创建一个简单的 users 表,包含 id, name, age 字段:

“`go
// … (接上面的 main 函数代码)

fmt.Println("Successfully connected to database:", dbFile)

// SQL 语句:创建 users 表
createTableSQL := `CREATE TABLE IF NOT EXISTS users (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" TEXT,
    "age" INTEGER
);`

// 执行创建表的 SQL 语句
_, err = db.Exec(createTableSQL)
if err != nil {
    log.Fatal("Error creating table:", err)
}

fmt.Println("Table 'users' created successfully (or already exists).")

// 后续的插入、查询等操作

}
“`

解释:

  • CREATE TABLE IF NOT EXISTS users: 创建一个名为 users 的表。IF NOT EXISTS 是一个非常有用的子句,如果表已经存在,它会跳过创建操作而不是报错。
  • "id" INTEGER PRIMARY KEY AUTOINCREMENT: 定义一个名为 id 的整数字段作为主键,并设置它为自增。在 SQLite 中,INTEGER PRIMARY KEY AUTOINCREMENT 字段会自动生成唯一的、递增的值。
  • "name" TEXT: 定义一个名为 name 的文本字段。
  • "age" INTEGER: 定义一个名为 age 的整数字段。
  • db.Exec(createTableSQL): 执行 SQL 语句。第一个返回值是 sql.Result,通常用于获取自增 ID 或影响的行数,但对于 CREATE TABLE 通常不需要。第二个返回值是错误。

运行这段代码,你的 example.db 文件中就会创建 users 表了。

5. 插入数据 (INSERT)

向表中插入数据是常见的操作。同样,我们可以使用 db.Exec()。然而,直接将变量拼接到 SQL 字符串中是非常危险的,会导致 SQL 注入漏洞。正确的做法是使用参数化查询(Prepared Statements)。

Go 的 database/sql 包支持在 SQL 语句中使用占位符来代表参数。对于 SQLite,占位符通常是 ?

5.1 使用 Exec 插入单条数据 (推荐方式)

“`go
// … (接上面的代码)

// 插入数据
insertSQL := `INSERT INTO users(name, age) VALUES (?, ?)`

// 执行插入操作,并传递参数
result, err := db.Exec(insertSQL, "Alice", 30)
if err != nil {
    log.Fatal("Error inserting data:", err)
}

// 获取新插入记录的 ID
lastID, err := result.LastInsertId()
if err != nil {
    log.Fatal("Error getting last insert ID:", err)
}
fmt.Printf("Inserted user with ID: %d\n", lastID)

result, err = db.Exec(insertSQL, "Bob", 25)
if err != nil {
    log.Fatal("Error inserting data:", err)
}
lastID, err = result.LastInsertId()
if err != nil {
    log.Fatal("Error getting last insert ID:", err)
}
fmt.Printf("Inserted user with ID: %d\n", lastID)

// 再次插入一个,演示不同年龄
result, err = db.Exec(insertSQL, "Charlie", 35)
if err != nil {
    log.Fatal("Error inserting data:", err)
}
lastID, err = result.LastInsertId()
if err != nil {
    log.Fatal("Error getting last insert ID:", err)
}
fmt.Printf("Inserted user with ID: %d\n", lastID)

// … 后续查询操作
“`

解释:

  • INSERT INTO users(name, age) VALUES (?, ?): SQL 语句中使用 ? 作为占位符。
  • db.Exec(insertSQL, "Alice", 30): 将 insertSQL 作为第一个参数,后面的 "Alice"30 分别对应 SQL 语句中的第一个和第二个 ?database/sql 包和驱动会负责安全地将这些值传递给数据库,避免 SQL 注入。
  • result.LastInsertId(): 对于支持自增主键的数据库(如 SQLite),Exec 返回的 sql.Result 对象可以用来获取最后插入行的自增 ID。

5.2 使用预处理语句 (Prepared Statement) 插入多条数据

如果你需要执行多次相似的 SQL 操作(特别是循环中),使用预处理语句(Prepared Statement)会更高效。db.Prepare() 方法会向数据库发送一次 SQL 语句,数据库会对其进行解析、优化并缓存执行计划。之后,你可以多次调用该预处理语句的 Exec()Query() 方法,只传递参数,而无需重复解析 SQL。

“`go
// … (接上面的代码)

// 插入多条数据,使用预处理语句
insertStmt, err := db.Prepare(`INSERT INTO users(name, age) VALUES (?, ?)`)
if err != nil {
    log.Fatal("Error preparing insert statement:", err)
}
// 确保在函数/方法退出前关闭预处理语句
defer insertStmt.Close()

// 使用预处理语句插入数据
result, err = insertStmt.Exec("David", 28)
if err != nil {
    log.Fatal("Error inserting David:", err)
}
lastID, err = result.LastInsertId()
fmt.Printf("Inserted user David with ID: %d\n", lastID)

result, err = insertStmt.Exec("Eve", 22)
if err != nil {
    log.Fatal("Error inserting Eve:", err)
}
lastID, err = result.LastInsertId()
fmt.Printf("Inserted user Eve with ID: %d\n", lastID)

// … 后续查询操作
“`

解释:

  • db.Prepare(...): 创建一个预处理语句。它返回一个 *sql.Stmt 对象。
  • defer insertStmt.Close(): 务必在使用完毕后关闭预处理语句。虽然底层驱动可能会自动管理,但显式关闭是最佳实践,可以释放数据库资源。
  • insertStmt.Exec("David", 28): 调用预处理语句的 Exec 方法,只传递参数。这比重复调用 db.Exec 效率更高,因为 SQL 解析和准备只执行了一次。

6. 查询数据 (SELECT)

查询数据是最复杂的操作之一,因为你需要处理返回的多行或单行结果。database/sql 提供了 Query()QueryRow() 方法。

6.1 查询多行数据 (Query)

使用 db.Query()stmt.Query() 方法执行 SELECT 语句,它返回一个 *sql.Rows 对象,代表查询结果集。你需要迭代这个对象来获取每一行的数据。

“`go
// … (接上面的代码)

// 查询所有用户
rows, err := db.Query("SELECT id, name, age FROM users")
if err != nil {
    log.Fatal("Error querying users:", err)
}
// 确保在处理完结果或发生错误后关闭 rows
defer rows.Close()

fmt.Println("\n--- All Users ---")
// 迭代结果集
for rows.Next() {
    var id int
    var name string
    var age int

    // Scan 将当前行的数据扫描到指定的变量中
    err = rows.Scan(&id, &name, &age)
    if err != nil {
        // 处理扫描错误,通常意味着数据类型不匹配或列顺序不对
        log.Println("Error scanning user row:", err)
        continue // 跳过当前行,继续下一行
    }

    fmt.Printf("ID: %d, Name: %s, Age: %d\n", id, name, age)
}

// 迭代完成后,务必检查 rows.Err() 是否有错误发生
// 这包括网络错误、在 Next() 期间发生的错误等
err = rows.Err()
if err != nil {
    log.Fatal("Error after iterating rows:", err)
}
fmt.Println("-----------------")

// … 后续更新/删除操作
“`

解释:

  • db.Query("SELECT id, name, age FROM users"): 执行查询语句并返回 *sql.Rows 对象。
  • defer rows.Close(): 非常重要!及时关闭 *sql.Rows 会释放底层数据库连接,使其可以被连接池复用。如果忘记关闭,可能会导致连接耗尽。
  • for rows.Next(): 迭代结果集的每一行。Next() 返回 true 如果有下一行可读,返回 false 如果没有更多行或发生错误。
  • rows.Scan(&id, &name, &age): 将当前行的列值扫描到对应的 Go 变量中。参数必须是变量的指针,并且顺序和类型应与 SELECT 语句中的列对应。
  • rows.Err(): 在 for 循环结束后, 务必 调用 rows.Err() 来检查在迭代过程中是否发生了除 io.EOF 之外的错误。

6.2 查询单行数据 (QueryRow)

如果你确定查询只会返回最多一行数据(例如通过主键查询),可以使用 db.QueryRow()stmt.QueryRow()。它直接返回一个 *sql.Row 对象。

“`go
// … (接上面的代码)

// 查询特定 ID 的用户 (例如 ID 为 1 的用户)
userIDToFind := 1
var foundName string
var foundAge int

// QueryRow 执行查询并期望最多返回一行
row := db.QueryRow("SELECT name, age FROM users WHERE id = ?", userIDToFind)

// Scan 将查询结果扫描到变量中
err = row.Scan(&foundName, &foundAge)
if err != nil {
    // 特别处理 sql.ErrNoRows 错误
    if err == sql.ErrNoRows {
        fmt.Printf("\nUser with ID %d not found.\n", userIDToFind)
    } else {
        // 处理其他扫描错误
        log.Fatal("Error scanning single user row:", err)
    }
} else {
    fmt.Printf("\nFound user with ID %d: Name: %s, Age: %d\n", userIDToFind, foundName, foundAge)
}

// … 后续更新/删除操作
“`

解释:

  • db.QueryRow("SELECT name, age FROM users WHERE id = ?", userIDToFind): 执行单行查询。注意参数的使用方式与 ExecQuery 类似。
  • row.Scan(&foundName, &foundAge): 从单行结果中扫描数据。QueryRow 返回的 *sql.Row 对象没有 Close() 方法,因为它是为单行结果设计的,会自动管理资源。
  • row.Scan() 方法返回错误。如果查询没有返回任何行,错误将是 sql.ErrNoRows。你需要显式地检查这个错误。如果发生了其他错误(如数据库错误),也会通过 Scan 返回。

6.3 处理 NULL 值

数据库字段可能允许 NULL 值。Go 的基本类型(如 int, string)不能直接表示 NULL。如果你尝试将数据库中的 NULL 值扫描到非指针或非 sql.Null 类型的 Go 变量中,Scan 方法会返回错误。

为了处理 NULL 值,你可以使用 database/sql 包提供的特殊类型,如 sql.NullString, sql.NullInt64, sql.NullBool, sql.NullTime 等。这些类型都有一个 Valid 字段和一个 Value 字段(例如 String)。Validtrue 表示值非 NULL 且存储在 Value 字段中,Validfalse 表示值为 NULL。

假设 age 字段允许 NULL:

“`go
// … (查询多行或单行时)

// 假设 age 字段可能为 NULL
var id int
var name string
var age sql.NullInt64 // 使用 sql.NullInt64 处理可能为 NULL 的整数

err = rows.Scan(&id, &name, &age)
if err != nil {
    log.Println("Error scanning user row (with NullInt64):", err)
    // ... handle error ...
}

fmt.Printf("ID: %d, Name: %s", id, name)
if age.Valid {
    fmt.Printf(", Age: %d\n", age.Int64) // 如果 Valid 为 true,使用 Int64 访问值
} else {
    fmt.Printf(", Age: NULL\n") // 如果 Valid 为 false,表示 NULL
}

// 或者对于单行查询
var foundName string
var foundAge sql.NullInt64

row := db.QueryRow(“SELECT name, age FROM users WHERE id = ?”, userIDToFind)
err = row.Scan(&foundName, &foundAge)
if err != nil {
// … handle sql.ErrNoRows or other errors …
} else {
fmt.Printf(“Found user: Name: %s”, foundName)
if foundAge.Valid {
fmt.Printf(“, Age: %d\n”, foundAge.Int64)
} else {
fmt.Printf(“, Age: NULL\n”)
}
fmt.Println()
}
“`

7. 更新数据 (UPDATE)

更新数据同样使用 db.Exec() 或预处理语句的 Exec() 方法。

“`go
// … (接上面的代码)

// 更新 ID 为 1 的用户的年龄
userIDToUpdate := 1
newAge := 31

updateSQL := `UPDATE users SET age = ? WHERE id = ?`

result, err = db.Exec(updateSQL, newAge, userIDToUpdate)
if err != nil {
    log.Fatal("Error updating user:", err)
}

// 获取受影响的行数
rowsAffected, err := result.RowsAffected()
if err != nil {
    log.Fatal("Error getting rows affected:", err)
}

fmt.Printf("\nUpdated %d row(s) for user ID %d.\n", rowsAffected, userIDToUpdate)

// … 后续删除操作
“`

解释:

  • UPDATE users SET age = ? WHERE id = ?: 标准的 SQL UPDATE 语句,使用 ? 占位符。
  • db.Exec(updateSQL, newAge, userIDToUpdate): 执行更新,传递新年龄和用户 ID 作为参数。
  • result.RowsAffected(): sql.Result 对象的 RowsAffected() 方法返回受 UPDATEDELETE 语句影响的行数。

8. 删除数据 (DELETE)

删除数据也是使用 db.Exec() 或预处理语句的 Exec() 方法。

“`go
// … (接上面的代码)

// 删除 ID 为 2 的用户
userIDToDelete := 2

deleteSQL := `DELETE FROM users WHERE id = ?`

result, err = db.Exec(deleteSQL, userIDToDelete)
if err != nil {
    log.Fatal("Error deleting user:", err)
}

// 获取受影响的行数
rowsAffected, err = result.RowsAffected()
if err != nil {
    log.Fatal("Error getting rows affected for delete:", err)
}

fmt.Printf("Deleted %d row(s) for user ID %d.\n", rowsAffected, userIDToDelete)

// … (main 函数结束)
} // main 函数结束括号
“`

解释:

  • DELETE FROM users WHERE id = ?: 标准的 SQL DELETE 语句,使用 ? 占位符。
  • db.Exec(deleteSQL, userIDToDelete): 执行删除,传递用户 ID 作为参数。
  • result.RowsAffected(): 返回被删除的行数。

9. 事务处理 (Transactions)

事务是一组数据库操作,它们要么全部成功,要么全部失败(原子性)。在 Go 中,使用 sql.DBBegin()BeginTx() 方法来开始一个事务。

“`go
// … (在 main 函数中,创建表之后,CRUD 之前或之间)

fmt.Println("\n--- Transaction Example ---")

// 开始一个事务
tx, err := db.Begin()
if err != nil {
    log.Fatal("Error beginning transaction:", err)
}

// 使用 defer Rollback() 是一个常见的模式,确保在函数/方法退出时回滚
// 如果事务成功并调用了 Commit(),Rollback() 将成为空操作
defer tx.Rollback()

// 在事务中使用 tx 对象的 Exec, Query, Prepare 等方法
// 插入一个新用户
insertTxSQL := `INSERT INTO users(name, age) VALUES (?, ?)`
result, err = tx.Exec(insertTxSQL, "Frank", 40)
if err != nil {
    log.Fatal("Error inserting Frank in transaction:", err) // 如果出错,defer 会执行 Rollback()
}
frankID, err := result.LastInsertId()
if err != nil {
    log.Fatal("Error getting Frank's ID in transaction:", err) // 如果出错,defer 会执行 Rollback()
}
fmt.Printf("Inserted Frank with ID: %d within transaction.\n", frankID)

// 更新另一个用户的年龄
updateTxSQL := `UPDATE users SET age = ? WHERE id = ?`
_, err = tx.Exec(updateTxSQL, 99, 1) // 更新 ID 1 的用户年龄到 99
if err != nil {
    log.Fatal("Error updating user ID 1 in transaction:", err) // 如果出错,defer 会执行 Rollback()
}
fmt.Println("Attempted to update user ID 1 to age 99 within transaction.")


// 模拟一个会出错的操作 (例如插入重复的主键,虽然此处 id 是自增的不会重复,但可以模拟其他逻辑错误)
// 例如,尝试执行一个错误的 SQL:
// _, err = tx.Exec(`INSERT INTO non_existent_table (col) VALUES (?)`, "test")
// if err != nil {
//     log.Println("Simulating an error within transaction:", err)
//     // defer tx.Rollback() 会在此处被触发,事务会回滚
//     return // 或其他错误处理
// }


// 如果所有操作都成功,提交事务
err = tx.Commit()
if err != nil {
    log.Fatal("Error committing transaction:", err)
}
fmt.Println("Transaction committed successfully.")

// 现在查询一下,ID 1 的用户年龄应该是 99,Frank 应该也存在了
fmt.Println("\n--- Users after Transaction ---")
// ... (这里可以再次执行查询所有用户的代码来验证)
rows, err = db.Query("SELECT id, name, age FROM users")
if err != nil {
    log.Fatal("Error querying users after transaction:", err)
}
defer rows.Close()

for rows.Next() {
    var id int
    var name string
    var age sql.NullInt64 // 使用 NullInt64 以防万一
    err = rows.Scan(&id, &name, &age)
    if err != nil {
        log.Println("Error scanning user row after transaction:", err)
        continue
    }
    fmt.Printf("ID: %d, Name: %s", id, name)
    if age.Valid {
        fmt.Printf(", Age: %d\n", age.Int64)
    } else {
        fmt.Printf(", Age: NULL\n")
    }
}
err = rows.Err()
if err != nil {
    log.Fatal("Error after iterating rows after transaction:", err)
}
fmt.Println("-----------------------------")

// … (main 函数结束)
}
“`

解释:

  • db.Begin(): 开始一个事务,返回 *sql.Tx 对象。
  • defer tx.Rollback(): 这是处理事务错误的关键模式。如果在 Commit() 被调用之前发生任何错误并导致函数提前返回,Rollback() 会被执行,撤销事务中的所有操作。如果 Commit() 成功执行,Rollback()defer 中被调用时,它会检测到事务已经提交,并安全地不做任何事情。
  • 在事务中,你必须使用 *sql.Tx 对象的方法 (tx.Exec(), tx.Query(), tx.Prepare()) 而不是 *sql.DB 对象的方法来执行数据库操作。
  • tx.Commit(): 如果所有操作都成功,调用 Commit() 提交事务,使更改永久生效。
  • tx.Rollback(): 取消事务中的所有操作,回滚到事务开始前的状态。

事务是保证数据一致性的重要手段,尤其是在需要执行多个相互依赖的数据库操作时。

10. 使用 Context

在现代 Go 应用,特别是在 web 服务中,使用 context.Context 来处理请求取消、超时和截止日期是标准做法。database/sql 包的大多数方法都有一个 Context 版本,例如 ExecContext, QueryContext, QueryRowContext, PrepareContext, BeginTx (它接受 Context)。

将 Context 集成到数据库操作中可以让你在外部信号(如 HTTP 请求取消)到达时,优雅地中断正在进行的数据库查询或操作,避免不必要的资源消耗。

“`go
package main

import (
“context”
“database/sql”
“fmt”
_ “github.com/mattn/go-sqlite3”
“log”
“os”
“time”
)

func main() {
dbFile := “./example.db”
// os.Remove(dbFile) // 演示时可能需要清理旧文件

db, err := sql.Open("sqlite3", dbFile)
if err != nil {
    log.Fatal("Error opening database:", err)
}
defer db.Close()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // 设置一个 5 秒的超时
defer cancel() // 确保取消 Context

// 使用带 Context 的 Ping
err = db.PingContext(ctx)
if err != nil {
    log.Fatal("Error connecting to database with context:", err)
}
fmt.Println("Successfully connected to database with context:", dbFile)

// 使用带 Context 的 Exec 创建表
createTableSQL := `CREATE TABLE IF NOT EXISTS context_users (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" TEXT
);`
_, err = db.ExecContext(ctx, createTableSQL)
if err != nil {
    log.Fatal("Error creating context_users table with context:", err)
}
fmt.Println("Table 'context_users' created successfully.")

// 使用带 Context 的 Prepare 和 Exec 插入数据
insertStmt, err := db.PrepareContext(ctx, `INSERT INTO context_users(name) VALUES (?)`)
if err != nil {
    log.Fatal("Error preparing insert statement with context:", err)
}
defer insertStmt.Close()

_, err = insertStmt.ExecContext(ctx, "ContextUser1")
if err != nil {
    log.Fatal("Error inserting ContextUser1 with context:", err)
}
fmt.Println("Inserted ContextUser1 with context.")

// 使用带 Context 的 Query 查询数据
rows, err := db.QueryContext(ctx, "SELECT id, name FROM context_users")
if err != nil {
    log.Fatal("Error querying context_users with context:", err)
}
defer rows.Close()

fmt.Println("\n--- Context Users ---")
for rows.Next() {
    var id int
    var name string
    err = rows.Scan(&id, &name)
    if err != nil {
        log.Println("Error scanning context_user row with context:", err)
        continue
    }
    fmt.Printf("ID: %d, Name: %s\n", id, name)
}
err = rows.Err()
if err != nil {
    log.Fatal("Error after iterating context_user rows with context:", err)
}
fmt.Println("---------------------")

// 模拟一个超时场景(如果需要)
// ctxSlow, cancelSlow := context.WithTimeout(context.Background(), 1*time.Millisecond)
// defer cancelSlow()
//
// // SQLite 通常太快了,很难模拟超时。在真实数据库中,你可以执行一个耗时查询
// // 例如:SELECT * FROM context_users WHERE id IN (SELECT id FROM context_users LIMIT 10000000);
// fmt.Println("\nAttempting a query that might timeout...")
// _, err = db.QueryContext(ctxSlow, "SELECT id, name FROM context_users LIMIT 1")
// if err != nil {
//  if err == context.DeadlineExceeded {
//      fmt.Println("Query timed out as expected.")
//  } else {
//      log.Printf("Query failed with unexpected error: %v\n", err)
//  }
// } else {
//  fmt.Println("Query completed before timeout (SQLite is fast!).")
// }

}
“`

解释:

  • context.WithTimeout(context.Background(), 5*time.Second): 创建一个带有 5 秒超时的 Context。context.Background() 是根 Context,通常用于应用的顶层。
  • defer cancel(): 返回的 cancel 函数应该在 Context 不再需要时调用,以释放相关资源。
  • 所有带有 Context 后缀的方法 (PingContext, ExecContext, QueryContext, PrepareContext) 都接受 Context 作为第一个参数。
  • 如果在数据库操作完成之前,传递的 Context 被取消或超时,数据库操作会中断并返回 Context 相关的错误(例如 context.Canceledcontext.DeadlineExceeded)。

在实际应用中,特别是 web 服务中,你应该将 HTTP 请求的 Context 传递到处理数据库操作的函数中。

11. 最佳实践和注意事项

  • 永远检查错误: 在 Go 中,数据库操作返回错误是非常常见的。你必须检查每一个操作的错误,并根据需要进行处理或返回。
  • 使用参数化查询: 避免手动拼接 SQL 字符串来插入参数,这可以防止 SQL 注入。始终使用占位符和参数传递。
  • 及时关闭资源: 使用 defer db.Close() 确保数据库连接被关闭。对于 *sql.Rows*sql.Stmt,也要使用 defer Close() 来释放资源。
  • 理解 sql.Open: 记住 sql.Open 不建立连接,真正的连接在第一次操作时建立。db.Ping() 可用于验证连接。
  • 连接池: sql.DB 对象内部管理一个连接池。它被设计成可以安全地被多个 Goroutine 并发使用。你应该在应用程序的生命周期内只创建一个 sql.DB 实例,并在需要时传递它。
  • 处理 NULL 值: 使用 sql.Null 类型或指针类型来处理数据库中可能为 NULL 的字段。
  • 事务: 对于需要原子性的操作序列,务必使用事务。遵循 Begin() -> defer Rollback() -> operations -> Commit() 的模式。
  • Context: 在可取消或有截止日期的操作中使用 Context,这对于构建响应迅速的服务至关重要。
  • 并发: sql.DB 是并发安全的,但单个 *sql.Tx*sql.Rows 对象不是并发安全的。不要在多个 Goroutine 中共享同一个事务或结果集对象。预处理语句 *sql.Stmt 是并发安全的,可以安全地在多个 Goroutine 中使用同一个 Stmt 来执行操作。
  • SQLite 特定: SQLite 是文件锁定的,并发写入可能会导致 SQLITE_BUSY 错误。go-sqlite3 驱动通常会处理一些锁定重试,但高并发写入场景可能需要更高级的同步机制或考虑更强大的客户端-服务器数据库。并发读通常没有问题。

12. 总结

本教程详细介绍了在 Go 语言中使用 database/sql 包和 mattn/go-sqlite3 驱动操作 SQLite 数据库的各个方面。我们学习了如何连接数据库、创建表、执行基本的增删改查操作,并深入探讨了预处理语句、事务、错误处理以及 Context 的使用。

SQLite 因其零配置、文件存储的特性,与 Go 语言的简洁高效完美结合,非常适合作为小型应用、测试、嵌入式场景甚至本地开发的数据存储方案。掌握 Go 与 SQLite 的交互,将为你在构建各类应用程序时提供一个强大而灵活的持久化选择。

记住,实践是最好的老师。尝试运行本教程中的代码示例,修改它们,并尝试构建更复杂的数据库操作,这将巩固你的学习。当你需要处理更复杂的模型或大量数据时,可以考虑使用 ORM (Object-Relational Mapper) 库,如 GORM 或 sqlboiler,它们可以在更高层面抽象数据库操作,但理解底层的 database/sql 包是使用 ORM 的坚实基础。

祝你在 Go 语言数据库编程的旅程中一切顺利!


发表评论

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

滚动至顶部