数据库操作
第27课:数据库操作
生活类比
想象你经营一家图书馆——
- 数据库就是图书馆的仓库,所有书籍(数据)都整齐地存放在书架上。
database/sql接口就像图书馆管理员的工作手册,规定了"怎么放书、怎么找书、怎么借书"的标准流程,不管仓库是木架(SQLite)还是钢架(MySQL),手册都通用。- 驱动(Driver) 是不同仓库的具体钥匙——SQLite 钥匙打开本地小仓库,MySQL 钥匙打开远程大仓库。
- 预编译语句就像提前打印好的借书单模板,每次只需填入姓名和书名,既快又安全。
- 事务就像"先借后还"的原子操作——要么全部完成,要么全部回滚,不会出现"借了但没登记"的尴尬。
学完本课,你就能用 Go 自如地与数据库打交道了。
核心概念
| 概念 | 说明 |
|---|---|
database/sql |
Go 标准库,定义了统一的数据库操作接口 |
| 驱动(Driver) | 实现 database/sql/driver 接口的第三方包 |
sql.Open() |
创建数据库连接池(不会立即建立连接) |
sql.DB |
数据库连接池对象,线程安全 |
Query / QueryRow |
执行查询语句,返回结果集 |
Exec |
执行非查询语句(INSERT/UPDATE/DELETE) |
Prepare |
预编译语句,提高重复执行的性能 |
Tx |
事务对象,支持 Commit 和 Rollback |
| 连接池 | 复用连接,避免频繁建立/关闭连接的开销 |
基本语法与用法
1. 导入驱动
GO
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // SQLite 驱动(匿名导入,仅注册驱动)
)
💡 提示:驱动必须用
_ 匿名导入,因为它只需要执行 init() 函数向 database/sql 注册自身,我们不直接调用驱动的函数。
2. 打开数据库
GO
db, err := sql.Open("sqlite3", "./mydb.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 验证连接是否可用
if err = db.Ping(); err != nil {
log.Fatal("无法连接数据库:", err)
}
💡 提示:
sql.Open() 不会立即建立连接,只是初始化连接池配置。用 Ping() 才会真正尝试连接。
3. 建表
GO
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER DEFAULT 0
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatal("建表失败:", err)
}
4. CRUD 操作
GO
// ---- INSERT ----
result, err := db.Exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"张三", "zhangsan@example.com", 25)
if err != nil {
log.Fatal(err)
}
id, _ := result.LastInsertId() // 获取自增 ID
fmt.Println("插入成功, ID:", id)
// ---- SELECT ----
var name, email string
var age int
err = db.QueryRow("SELECT name, email, age FROM users WHERE id = ?", id).
Scan(&name, &email, &age)
if err != nil {
log.Fatal(err)
}
fmt.Printf("查询结果: %s, %s, %d岁\n", name, email, age)
// ---- UPDATE ----
_, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 26, id)
if err != nil {
log.Fatal(err)
}
// ---- DELETE ----
_, err = db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
log.Fatal(err)
}
5. 预编译语句
GO
stmt, err := db.Prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// 可多次复用同一个 stmt,性能更佳
stmt.Exec("李四", "lisi@example.com", 30)
stmt.Exec("王五", "wangwu@example.com", 28)
💡 提示:预编译语句既提升性能(数据库只需解析一次 SQL),又防止 SQL 注入。
6. 事务处理
GO
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// 在事务中执行多条语句
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
tx.Rollback() // 出错则回滚
log.Fatal(err)
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
if err = tx.Commit(); err != nil { // 全部成功则提交
log.Fatal(err)
}
fmt.Println("事务提交成功")
7. 连接池配置
GO
db.SetMaxOpenConns(25) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
db.SetConnMaxIdleTime(3 * time.Minute) // 空闲连接最大存活时间
💡 提示:合理配置连接池能显著提升高并发场景下的性能。
MaxOpenConns 设置过小会导致请求排队,设置过大会耗尽数据库资源。
实战演练
示例:SQLite 用户管理(难度⭐)
一个完整的用户管理示例,演示基本的 CRUD 操作。
GO
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3" // SQLite 驱动
)
// User 用户结构体
type User struct {
ID int
Name string
Email string
Age int
}
func main() {
// 打开(或创建)SQLite 数据库
db, err := sql.Open("sqlite3", "./users.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 建表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER DEFAULT 0
)
`)
if err != nil {
log.Fatal("建表失败:", err)
}
// 插入用户
result, err := db.Exec(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"张三", "zhangsan@example.com", 25,
)
if err != nil {
log.Fatal("插入失败:", err)
}
id, _ := result.LastInsertId()
fmt.Printf("插入用户成功, ID: %d\n", id)
// 查询单个用户
var u User
err = db.QueryRow("SELECT id, name, email, age FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Name, &u.Email, &u.Age)
if err != nil {
log.Fatal("查询失败:", err)
}
fmt.Printf("查询结果: %+v\n", u)
// 更新用户年龄
_, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 26, id)
if err != nil {
log.Fatal("更新失败:", err)
}
fmt.Println("更新成功")
// 查询所有用户
rows, err := db.Query("SELECT id, name, email, age FROM users")
if err != nil {
log.Fatal("查询全部失败:", err)
}
defer rows.Close()
fmt.Println("\n所有用户:")
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age); err != nil {
log.Fatal(err)
}
fmt.Printf(" ID:%d 姓名:%s 邮箱:%s 年龄:%d\n",
user.ID, user.Name, user.Email, user.Age)
}
// 删除用户
_, err = db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
log.Fatal("删除失败:", err)
}
fmt.Println("删除用户成功")
}
TEXT
插入用户成功, ID: 1
查询结果: {ID:1 Name:张三 Email:zhangsan@example.com Age:25}
更新成功
所有用户:
ID:1 姓名:张三 邮箱:zhangsan@example.com 年龄:26
删除用户成功
示例:CRUD 封装 + 预编译语句(难度⭐⭐)
将数据库操作封装为结构体方法,使用预编译语句提高性能。
GO
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
// User 用户模型
type User struct {
ID int
Name string
Email string
Age int
}
// UserDAO 用户数据访问对象
type UserDAO struct {
db *sql.DB
insertStmt *sql.Stmt
getByID *sql.Stmt
updateStmt *sql.Stmt
deleteStmt *sql.Stmt
}
// NewUserDAO 创建 DAO 并初始化预编译语句
func NewUserDAO(db *sql.DB) (*UserDAO, error) {
dao := &UserDAO{db: db}
// 初始化表
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER DEFAULT 0
)
`)
if err != nil {
return nil, err
}
// 预编译所有常用语句
dao.insertStmt, err = db.Prepare(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
if err != nil {
return nil, err
}
dao.getByID, err = db.Prepare(
"SELECT id, name, email, age FROM users WHERE id = ?")
if err != nil {
return nil, err
}
dao.updateStmt, err = db.Prepare(
"UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?")
if err != nil {
return nil, err
}
dao.deleteStmt, err = db.Prepare(
"DELETE FROM users WHERE id = ?")
if err != nil {
return nil, err
}
return dao, nil
}
// Close 关闭所有预编译语句
func (d *UserDAO) Close() {
d.insertStmt.Close()
d.getByID.Close()
d.updateStmt.Close()
d.deleteStmt.Close()
}
// Create 插入新用户
func (d *UserDAO) Create(u *User) error {
result, err := d.insertStmt.Exec(u.Name, u.Email, u.Age)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
u.ID = int(id)
return nil
}
// GetByID 根据 ID 查询用户
func (d *UserDAO) GetByID(id int) (*User, error) {
u := &User{}
err := d.getByID.QueryRow(id).Scan(&u.ID, &u.Name, &u.Email, &u.Age)
if err != nil {
return nil, err
}
return u, nil
}
// Update 更新用户信息
func (d *UserDAO) Update(u *User) error {
_, err := d.updateStmt.Exec(u.Name, u.Email, u.Age, u.ID)
return err
}
// Delete 删除用户
func (d *UserDAO) Delete(id int) error {
_, err := d.deleteStmt.Exec(id)
return err
}
// ListAll 查询所有用户
func (d *UserDAO) ListAll() ([]User, error) {
rows, err := d.db.Query("SELECT id, name, email, age FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
func main() {
db, err := sql.Open("sqlite3", ":memory:") // 内存数据库,便于演示
if err != nil {
log.Fatal(err)
}
defer db.Close()
dao, err := NewUserDAO(db)
if err != nil {
log.Fatal(err)
}
defer dao.Close()
// 批量插入
users := []User{
{Name: "张三", Email: "zhangsan@example.com", Age: 25},
{Name: "李四", Email: "lisi@example.com", Age: 30},
{Name: "王五", Email: "wangwu@example.com", Age: 28},
}
for i := range users {
if err := dao.Create(&users[i]); err != nil {
log.Printf("插入失败: %v", err)
continue
}
fmt.Printf("插入: %s (ID=%d)\n", users[i].Name, users[i].ID)
}
// 查询并修改
u, err := dao.GetByID(1)
if err != nil {
log.Fatal("查询失败:", err)
}
fmt.Printf("\n查询 ID=1: %+v\n", u)
u.Age = 26
if err := dao.Update(u); err != nil {
log.Fatal("更新失败:", err)
}
fmt.Printf("更新后: %+v\n", u)
// 列出所有
all, _ := dao.ListAll()
fmt.Println("\n所有用户:")
for _, user := range all {
fmt.Printf(" [%d] %s | %s | %d岁\n",
user.ID, user.Name, user.Email, user.Age)
}
// 删除
dao.Delete(2)
fmt.Println("\n删除 ID=2 后:")
all, _ = dao.ListAll()
for _, user := range all {
fmt.Printf(" [%d] %s | %s | %d岁\n",
user.ID, user.Name, user.Email, user.Age)
}
}
TEXT
插入: 张三 (ID=1)
插入: 李四 (ID=2)
插入: 王五 (ID=3)
查询 ID=1: {ID:1 Name:张三 Email:zhangsan@example.com Age:25}
更新后: {ID:1 Name:张三 Email:zhangsan@example.com Age:26}
所有用户:
[1] 张三 | zhangsan@example.com | 26岁
[2] 李四 | lisi@example.com | 30岁
[3] 王五 | wangwu@example.com | 28岁
删除 ID=2 后:
[1] 张三 | zhangsan@example.com | 26岁
[3] 王五 | wangwu@example.com | 28岁
示例:事务转账 + 连接池配置(难度⭐⭐⭐)
模拟银行转账场景,演示事务的原子性保障和连接池配置。
GO
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
// Account 银行账户
type Account struct {
ID int
Name string
Balance float64
}
func initDB(db *sql.DB) error {
// 建表
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
balance REAL NOT NULL DEFAULT 0
)
`)
return err
}
// Transfer 转账(事务操作)
// 从 fromID 转 amount 到 toID
func Transfer(db *sql.DB, fromID, toID int, amount float64) error {
// 开启事务
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开启事务失败: %w", err)
}
// 确保事务最终要么提交要么回滚
defer func() {
if err != nil {
tx.Rollback()
}
}()
// 检查转出账户余额
var fromBalance float64
err = tx.QueryRow("SELECT balance FROM accounts WHERE id = ?", fromID).Scan(&fromBalance)
if err != nil {
return fmt.Errorf("查询转出账户失败: %w", err)
}
if fromBalance < amount {
return fmt.Errorf("余额不足: 当前 %.2f, 需要 %.2f", fromBalance, amount)
}
// 扣除转出账户
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
if err != nil {
return fmt.Errorf("扣款失败: %w", err)
}
// 增加转入账户
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
if err != nil {
return fmt.Errorf("入账失败: %w", err)
}
// 模拟业务逻辑:记录操作日志(在同一事务中)
_, err = tx.Exec(
"INSERT INTO transfer_log (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)",
fromID, toID, amount, time.Now().Format(time.RFC3339),
)
if err != nil {
// 如果日志表不存在,忽略错误(仅作演示)
fmt.Printf(" [警告] 转账日志记录失败: %v\n", err)
}
// 提交事务
if err = tx.Commit(); err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}
// PrintBalances 打印所有账户余额
func PrintBalances(db *sql.DB) {
rows, err := db.Query("SELECT id, name, balance FROM accounts ORDER BY id")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
fmt.Println(" 账户余额:")
for rows.Next() {
var a Account
if err := rows.Scan(&a.ID, &a.Name, &a.Balance); err != nil {
log.Fatal(err)
}
fmt.Printf(" [%d] %-6s 余额: %.2f\n", a.ID, a.Name, a.Balance)
}
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 配置连接池
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(3 * time.Minute)
fmt.Println("✓ 连接池已配置:")
fmt.Println(" 最大连接数: 10")
fmt.Println(" 最大空闲数: 5")
fmt.Println(" 连接存活期: 5 分钟")
fmt.Println()
// 初始化表
if err := initDB(db); err != nil {
log.Fatal(err)
}
// 创建转账日志表(可选)
db.Exec(`CREATE TABLE IF NOT EXISTS transfer_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_id INTEGER, to_id INTEGER,
amount REAL, created_at TEXT
)`)
// 插入初始账户
_, err = db.Exec("INSERT INTO accounts (name, balance) VALUES (?, ?), (?, ?)",
"张三", 1000.00, "李四", 500.00)
if err != nil {
log.Fatal("初始化账户失败:", err)
}
fmt.Println("=== 初始状态 ===")
PrintBalances(db)
fmt.Println()
// 正常转账:张三 -> 李四 转 300
fmt.Println("=== 张三 -> 李四 转账 300 ===")
if err := Transfer(db, 1, 2, 300); err != nil {
fmt.Printf("转账失败: %v\n", err)
} else {
fmt.Println("转账成功!")
}
PrintBalances(db)
fmt.Println()
// 余额不足转账:李四 -> 张三 转 900
fmt.Println("=== 李四 -> 张三 转账 900 ===")
if err := Transfer(db, 2, 1, 900); err != nil {
fmt.Printf("转账失败: %v\n", err)
} else {
fmt.Println("转账成功!")
}
PrintBalances(db)
fmt.Println()
// 再次正常转账
fmt.Println("=== 李四 -> 张三 转账 100 ===")
if err := Transfer(db, 2, 1, 100); err != nil {
fmt.Printf("转账失败: %v\n", err)
} else {
fmt.Println("转账成功!")
}
PrintBalances(db)
}
TEXT
✓ 连接池已配置:
最大连接数: 10
最大空闲数: 5
连接存活期: 5 分钟
=== 初始状态 ===
账户余额:
[1] 张三 余额: 1000.00
[2] 李四 余额: 500.00
=== 张三 -> 李四 转账 300 ===
转账成功!
账户余额:
[1] 张三 余额: 700.00
[2] 李四 余额: 800.00
=== 李四 -> 张三 转账 900 ===
转账失败: 余额不足: 当前 800.00, 需要 900.00
账户余额:
[1] 张三 余额: 700.00
[2] 李四 余额: 800.00
=== 李四 -> 张三 转账 100 ===
转账成功!
账户余额:
[1] 张三 余额: 800.00
[2] 李四 余额: 700.00
实际应用场景
场景1:用户注册与登录(MySQL)
使用 MySQL 存储用户信息,实现注册和密码验证。
GO
package main
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql" // MySQL 驱动
)
// hashPassword 简单的密码哈希(生产环境应使用 bcrypt)
func hashPassword(password string) string {
h := sha256.Sum256([]byte(password))
return hex.EncodeToString(h[:])
}
// UserRepository 用户仓库
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(dsn string) (*UserRepository, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// 配置连接池
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(10 * time.Minute)
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("数据库连接失败: %w", err)
}
return &UserRepository{db: db}, nil
}
func (r *UserRepository) Close() error {
return r.db.Close()
}
// Register 用户注册
func (r *UserRepository) Register(username, password, email string) (int64, error) {
hashedPwd := hashPassword(password)
result, err := r.db.Exec(
"INSERT INTO users (username, password_hash, email, created_at) VALUES (?, ?, ?, ?)",
username, hashedPwd, email, time.Now(),
)
if err != nil {
return 0, fmt.Errorf("注册失败: %w", err)
}
return result.LastInsertId()
}
// Login 用户登录验证
func (r *UserRepository) Login(username, password string) (int, error) {
hashedPwd := hashPassword(password)
var id int
var storedHash string
err := r.db.QueryRow(
"SELECT id, password_hash FROM users WHERE username = ?",
username,
).Scan(&id, &storedHash)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("用户不存在")
}
if err != nil {
return 0, fmt.Errorf("查询失败: %w", err)
}
if storedHash != hashedPwd {
return 0, fmt.Errorf("密码错误")
}
// 更新最后登录时间
_, _ = r.db.Exec(
"UPDATE users SET last_login = ? WHERE id = ?",
time.Now(), id,
)
return id, nil
}
func main() {
// DSN 格式: 用户名:密码@tcp(地址:端口)/数据库名?parseTime=true
dsn := "root:password@tcp(127.0.0.1:3306)/myapp?parseTime=true"
repo, err := NewUserRepository(dsn)
if err != nil {
log.Fatal("初始化失败:", err)
}
defer repo.Close()
// 注册
id, err := repo.Register("zhangsan", "mypassword123", "zhangsan@example.com")
if err != nil {
log.Fatal("注册失败:", err)
}
fmt.Printf("注册成功, 用户ID: %d\n", id)
// 登录
userID, err := repo.Login("zhangsan", "mypassword123")
if err != nil {
fmt.Printf("登录失败: %v\n", err)
} else {
fmt.Printf("登录成功, 用户ID: %d\n", userID)
}
// 密码错误
_, err = repo.Login("zhangsan", "wrongpassword")
if err != nil {
fmt.Printf("登录失败: %v\n", err)
}
}
场景2:分页查询与批量插入
高效处理大量数据的分页查询和批量插入。
GO
package main
import (
"database/sql"
"fmt"
"log"
"strings"
_ "github.com/mattn/go-sqlite3"
)
// Product 商品
type Product struct {
ID int
Name string
Price float64
Stock int
}
// PageResult 分页结果
type PageResult struct {
Items []Product
Total int
Page int
PageSize int
TotalPages int
}
// ProductStore 商品存储
type ProductStore struct {
db *sql.DB
}
func NewProductStore(db *sql.DB) (*ProductStore, error) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER DEFAULT 0
)
`)
if err != nil {
return nil, err
}
return &ProductStore{db: db}, nil
}
// BatchInsert 批量插入(使用事务提高性能)
func (s *ProductStore) BatchInsert(products []Product) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
stmt, err := tx.Prepare("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)")
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, p := range products {
_, err := stmt.Exec(p.Name, p.Price, p.Stock)
if err != nil {
tx.Rollback()
return fmt.Errorf("插入 %s 失败: %w", p.Name, err)
}
}
return tx.Commit()
}
// Paginate 分页查询
func (s *ProductStore) Paginate(page, pageSize int) (*PageResult, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
// 查询总数
var total int
err := s.db.QueryRow("SELECT COUNT(*) FROM products").Scan(&total)
if err != nil {
return nil, err
}
// 计算总页数
totalPages := total / pageSize
if total%pageSize > 0 {
totalPages++
}
// 查询当前页数据
offset := (page - 1) * pageSize
rows, err := s.db.Query(
"SELECT id, name, price, stock FROM products ORDER BY id LIMIT ? OFFSET ?",
pageSize, offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock); err != nil {
return nil, err
}
items = append(items, p)
}
return &PageResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// Search 按名称模糊搜索
func (s *ProductStore) Search(keyword string, page, pageSize int) (*PageResult, error) {
likePattern := "%" + keyword + "%"
var total int
err := s.db.QueryRow(
"SELECT COUNT(*) FROM products WHERE name LIKE ?", likePattern,
).Scan(&total)
if err != nil {
return nil, err
}
totalPages := total / pageSize
if total%pageSize > 0 {
totalPages++
}
offset := (page - 1) * pageSize
rows, err := s.db.Query(
"SELECT id, name, price, stock FROM products WHERE name LIKE ? ORDER BY id LIMIT ? OFFSET ?",
likePattern, pageSize, offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock); err != nil {
return nil, err
}
items = append(items, p)
}
return &PageResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
func printPage(result *PageResult) {
fmt.Printf(" 第 %d/%d 页 (共 %d 条)\n",
result.Page, result.TotalPages, result.Total)
for _, p := range result.Items {
fmt.Printf(" [%d] %-10s 价格: %6.2f 库存: %d\n",
p.ID, p.Name, p.Price, p.Stock)
}
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
store, err := NewProductStore(db)
if err != nil {
log.Fatal(err)
}
// 批量插入 50 个商品
var products []Product
names := []string{"键盘", "鼠标", "显示器", "耳机", "音箱",
"摄像头", "U盘", "硬盘", "内存条", "主板"}
for i := 0; i < 50; i++ {
products = append(products, Product{
Name: fmt.Sprintf("%s-%02d", names[i%len(names)], i+1),
Price: float64(50 + i*10),
Stock: 100 - i,
})
}
if err := store.BatchInsert(products); err != nil {
log.Fatal("批量插入失败:", err)
}
fmt.Printf("✓ 批量插入 %d 个商品成功\n\n", len(products))
// 分页查询
fmt.Println("=== 分页查询 (每页 10 条) ===")
for page := 1; page <= 3; page++ {
result, err := store.Paginate(page, 10)
if err != nil {
log.Fatal(err)
}
printPage(result)
fmt.Println()
}
// 搜索
fmt.Println("=== 搜索 '键盘' ===")
result, err := store.Search("键盘", 1, 10)
if err != nil {
log.Fatal(err)
}
printPage(result)
}
TEXT
✓ 批量插入 50 个商品成功
=== 分页查询 (每页 10 条) ===
第 1/5 页 (共 50 条)
[1] 键盘-01 价格: 50.00 库存: 100
[2] 鼠标-02 价格: 60.00 库存: 99
...
=== 搜索 '键盘' ===
第 1/1 页 (共 5 条)
[1] 键盘-01 价格: 50.00 库存: 100
[11] 键盘-11 价格: 150.00 库存: 90
[21] 键盘-21 价格: 250.00 库存: 80
[31] 键盘-31 价格: 350.00 库存: 70
[41] 键盘-41 价格: 450.00 库存: 60
❓ 常见问题
Q1:sql.Open() 报错 unknown driver 是什么原因?
驱动没有正确导入。确保使用匿名导入:
GO
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // 必须有这行
)
_ 表示只执行该包的 init() 函数(注册驱动),不直接引用包名。忘记导入驱动是最常见的新手错误。
Q2:为什么 QueryRow().Scan() 在没有数据时会报错?
当查询没有匹配行时,Scan() 返回 sql.ErrNoRows。需要单独处理:
GO
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 999).Scan(&name)
if err == sql.ErrNoRows {
fmt.Println("用户不存在")
} else if err != nil {
log.Fatal("查询出错:", err)
}
Q3:db.Close() 应该什么时候调用?
sql.DB 是连接池,应该在程序整个生命周期内保持打开,而不是每次操作后关闭。通常用 defer db.Close() 在 main() 函数开头:
GO
func main() {
db, err := sql.Open("sqlite3", "./mydb.db")
if err != nil {
log.Fatal(err)
}
defer db.Close() // 程序退出时才关闭
// ... 使用 db 进行各种操作
}
Q4:如何防止 SQL 注入?
永远不要用字符串拼接构建 SQL 语句,始终使用参数化查询:
GO
// ❌ 危险!存在 SQL 注入风险
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)
// ✓ 安全:使用参数占位符
db.Query("SELECT * FROM users WHERE name = ?", userInput)
占位符 ? 会由驱动自动转义,数据库会将其视为纯数据而非 SQL 代码。
📖 小节
本课我们学习了:
| 知识点 | 要点 |
|---|---|
database/sql |
Go 标准库统一接口,支持任意数据库驱动 |
| 驱动导入 | 必须用 _ 匿名导入,仅注册驱动 |
sql.Open |
创建连接池,不立即连接;用 Ping() 验证 |
| CRUD | Exec 执行写操作,Query/QueryRow 读取数据 |
| 预编译语句 | Prepare 提升性能、防止 SQL 注入 |
| 事务 | Begin→Exec→Commit/Rollback,保证原子性 |
| 连接池 | SetMaxOpenConns/SetMaxIdleConns 等方法调优 |
Scan |
将数据库列映射到 Go 变量 |
sql.ErrNoRows |
查询无结果时的特殊错误,需单独处理 |
掌握这些,你就具备了在 Go 项目中操作关系数据库的完整能力。
📝 作业
练习1:基础 CRUD(热身)
创建一个 SQLite 数据库,建立 books 表(字段:id, title, author, price, published_year),完成以下操作:
- 插入至少 5 本书
- 查询价格大于 50 元的书
- 更新某本书的价格
- 删除出版年份早于 2000 年的书
练习2:事务与错误处理
在练习1的基础上,实现一个 TransferBook 函数:将一个作者的所有书籍转移到另一个作者名下(用事务保证原子性)。要求:
- 如果原作者不存在,回滚并返回错误
- 更新成功后打印影响的行数
- 使用
defer+recover防止 panic 导致事务悬挂
练习3:综合挑战——迷你图书管理系统
构建一个命令行图书管理系统,要求:
- 支持增删改查和分页列表
- 使用 DAO 模式封装数据库操作
- 支持按书名模糊搜索和按价格范围筛选
- 使用预编译语句优化重复查询
- 配置合理的连接池参数
- 用结构化的错误处理替代
log.Fatal
下一课:第28课:项目部署 —— 学习如何将 Go 项目编译、打包并部署到服务器。



