第 51 课 - database/sql 基础
学习目标
通过本节课的学习,你将能够:
- 理解 Go 标准库
database/sql的作用和基本概念。 - 掌握使用
database/sql包连接数据库、执行查询和写入操作。 - 学会正确处理数据库操作中的错误和资源管理。
- 了解预编译语句(Prepare)的使用及其优势。
- 初步了解数据库连接池的基本配置。
核心概念
database/sql 是 Go 语言标准库中用于与关系型数据库交互的核心包。它提供了一套统一的接口,你可以通过导入不同的数据库驱动(如 github.com/go-sql-driver/mysql 或 github.com/mattn/go-sqlite3)来连接 MySQL、PostgreSQL、SQLite 等具体数据库。
关键概念:
- 驱动(Driver):具体的数据库实现,通常是一个第三方包。
- 数据源名称(DSN):连接数据库的字符串,格式因驱动而异(例如
"user:password@tcp(127.0.0.1:3306)/dbname")。 - 连接池:
database/sql会自动管理一组可复用的数据库连接,避免了频繁地打开和关闭连接。 - 核心方法:
db.Query():执行查询并返回多行结果。db.QueryRow():执行查询并预期返回最多一行结果。db.Exec():执行不返回行的语句(如 INSERT, UPDATE, DELETE)。db.Prepare():创建预编译语句,提高重复执行的效率并防止SQL注入。
代码示例
下面是一个使用 database/sql 连接 SQLite 数据库并进行基本操作的完整示例。请确保已安装 go-sqlite3 驱动。
package main
import (
"database/sql"
"fmt"
"log"
// 导入 SQLite 驱动。它在导入时通过 init() 函数自动注册。
// 下划线 `_` 表示我们只需要它的副作用(注册驱动),而不会直接使用包中的函数。
_ "github.com/mattn/go-sqlite3"
)
// User 结构体用于映射数据库中的 user 表行
type User struct {
ID int
Name string
Email string
}
func main() {
// 1. 打开数据库连接。
// DSN 对于 SQLite 是文件名。`./test.db` 会在当前目录创建或打开一个名为 test.db 的数据库文件。
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatalf("无法打开数据库: %v", err)
}
// 确保在 main 函数结束时关闭数据库连接池
defer db.Close()
// 2. 测试数据库连接是否真的可用
err = db.Ping()
if err != nil {
log.Fatalf("无法连接到数据库: %v", err)
}
fmt.Println("成功连接到数据库!")
// 3. 创建表(如果不存在)
createTableSQL := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatalf("创建表失败: %v", err)
}
fmt.Println("表 `users` 已就绪。")
// 4. 插入数据
insertSQL := `INSERT INTO users (name, email) VALUES (?, ?)`
res, err := db.Exec(insertSQL, "张三", "[email protected]")
if err != nil {
log.Fatalf("插入数据失败: %v", err)
}
id, _ := res.LastInsertId()
rowsAffected, _ := res.RowsAffected()
fmt.Printf("插入成功。ID: %d, 影响行数: %d\n", id, rowsAffected)
// 5. 查询单行数据
var user User
queryRowSQL := `SELECT id, name, email FROM users WHERE id = ?`
err = db.QueryRow(queryRowSQL, 1).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
// 特别注意:QueryRow 在找不到记录时返回 sql.ErrNoRows 错误
if err == sql.ErrNoRows {
fmt.Println("未找到ID为1的用户")
} else {
log.Fatalf("查询单行数据失败: %v", err)
}
} else {
fmt.Printf("查询到用户: %+v\n", user)
}
// 6. 查询多行数据
querySQL := `SELECT id, name, email FROM users`
rows, err := db.Query(querySQL)
if err != nil {
log.Fatalf("查询多行数据失败: %v", err)
}
// 关键:必须在使用完 rows 后关闭它,以释放连接回连接池
defer rows.Close()
var users []User
for rows.Next() {
var u User
// 将当前行的数据扫描到结构体字段中
err := rows.Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
log.Fatalf("扫描行数据失败: %v", err)
}
users = append(users, u)
}
// 检查遍历过程中是否出错
if err = rows.Err(); err != nil {
log.Fatalf("遍历结果集时出错: %v", err)
}
fmt.Printf("查询到所有用户: %v\n", users)
// 7. 使用预编译语句(Prepare)进行批量插入
insertStmt, err := db.Prepare(insertSQL)
if err != nil {
log.Fatalf("预编译语句失败: %v", err)
}
defer insertStmt.Close()
newUsers := []struct {
Name string
Email string
}{
{"李四", "[email protected]"},
{"王五", "[email protected]"},
}
for _, u := range newUsers {
_, err := insertStmt.Exec(u.Name, u.Email)
if err != nil {
log.Printf("批量插入用户 %s 失败: %v\n", u.Name, err)
continue
}
fmt.Printf("成功插入用户: %s\n", u.Name)
}
}
如何运行:
- 初始化一个 Go 模块:
go mod init example.com/sql-demo - 获取依赖:
go get github.com/mattn/go-sqlite3 - 运行程序:
go run main.go
实践练习
练习 1:基础查询
编写一个程序,连接到 SQLite 数据库 school.db,创建一个 students 表(包含 id, name, grade 字段),插入至少3条记录,然后查询并打印出所有 grade 大于 90 的学生信息。
- 预期输出示例:
优秀学生: - 姓名: 小明, 年级: 95 - 姓名: 小红, 年级: 92
练习 2:事务操作
在练习1的程序中添加一个函数 transferStudent(oldName, newGrade string)。该函数使用事务 (db.Begin()) 将一个学生的名字更新为 “已毕业” 并将其成绩设为 0。如果更新过程中发生任何错误,整个事务应回滚。
- 要求:编写代码并测试在事务提交前模拟错误(如
rollback())和正常提交两种情况。
练习 3:错误处理与 NULL值
在数据库中添加一个 student_email 字段(允许为空 NULL)。编写代码查询一个存在但 email 为 NULL 的学生,并使用 sql.NullString 类型正确扫描和打印出结果。
- 预期输出示例(当email为NULL时):
学生: 小华, 邮箱: <未设置>
常见错误
- 忘记检查错误:几乎每个
database/sql的方法都返回错误。忽略这些错误是程序不稳定和难以调试的主要原因。 - 资源泄露:忘记
defer rows.Close()或defer db.Close()。这会导致数据库连接无法归还到连接池,最终耗尽连接资源。 - SQL注入:直接使用
fmt.Sprintf拼接 SQL 字符串(如fmt.Sprintf("... WHERE name = '%s'", userInput))是极其危险的。务必使用参数化查询(?占位符)。 - 误解
QueryRow的错误:当QueryRow查不到记录时,它返回一个sql.ErrNoRows错误,而不是nil。必须显式检查这个特定错误。 - 忽略
rows.Err():在循环rows.Next()结束后,应调用rows.Err()来检查迭代过程中是否发生错误(如连接中断)。 - NULL 值处理:对于数据库中允许 NULL 的字段,不能直接扫描到普通 Go 类型(如
string),而应使用sql.NullString,sql.NullInt64等类型。
小结
本节课我们学习了 Go 语言操作关系型数据库的基础——database/sql 包:
- 它是标准接口:我们只需关心接口,通过驱动连接不同数据库。
- 连接管理:使用
sql.Open并传入正确的驱动名和 DSN 来建立连接。db.Ping()用于验证连接。 - 核心操作:掌握了
Exec(执行)、Query(查询多行)、QueryRow(查询单行)、Prepare(预编译)四个关键方法。 - 错误处理:理解了
sql.ErrNoRows等特定错误,并强调了持续检查错误的重要性。 - 资源管理:必须使用
defer关闭db、rows和Stmt对象,这是防止资源泄露的关键。 - 安全性:始终使用参数化查询(
?)来防止 SQL 注入。
掌握了这些基础,你就可以为后续更复杂的数据库操作(如连接池配置、ORM 框架使用、复杂事务处理)打下坚实的基础。下一课,我们将聚焦于如何使用这些知识来操作 PostgreSQL 数据库。