404 Not Found

404 Not Found


nginx

数据库操作

第27课:数据库操作

生活类比

想象你经营一家图书馆——

学完本课,你就能用 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 注入
事务 BeginExecCommit/Rollback,保证原子性
连接池 SetMaxOpenConns/SetMaxIdleConns 等方法调优
Scan 将数据库列映射到 Go 变量
sql.ErrNoRows 查询无结果时的特殊错误,需单独处理

掌握这些,你就具备了在 Go 项目中操作关系数据库的完整能力。


📝 作业

练习1:基础 CRUD(热身)

创建一个 SQLite 数据库,建立 books 表(字段:id, title, author, price, published_year),完成以下操作:

  1. 插入至少 5 本书
  2. 查询价格大于 50 元的书
  3. 更新某本书的价格
  4. 删除出版年份早于 2000 年的书

练习2:事务与错误处理

在练习1的基础上,实现一个 TransferBook 函数:将一个作者的所有书籍转移到另一个作者名下(用事务保证原子性)。要求:

练习3:综合挑战——迷你图书管理系统

构建一个命令行图书管理系统,要求:

  1. 支持增删改查和分页列表
  2. 使用 DAO 模式封装数据库操作
  3. 支持按书名模糊搜索和按价格范围筛选
  4. 使用预编译语句优化重复查询
  5. 配置合理的连接池参数
  6. 用结构化的错误处理替代 log.Fatal

下一课:第28课:项目部署 —— 学习如何将 Go 项目编译、打包并部署到服务器。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏