Skip to content

Go 数据库编程教程

📖 关于本教程本教程系统讲解 Go 操作 MySQL 的全流程:从安装 MySQL、基础 SQL,到 Go 官方 database/sql 接口、CRUD、事务、SQL Builder、批量插入、联合查询与翻页,配合完整可运行代码。


1. MySQL 安装和常用 SQL 语句

1.1 安装 MySQL

shell
# ==================== Docker 安装(推荐,最省事)====================
docker run -d \
  --name mysql8 \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=root123 \
  -e MYSQL_DATABASE=testdb \
  -e MYSQL_USER=gouser \
  -e MYSQL_PASSWORD=gopass123 \
  -v mysql_data:/var/lib/mysql \
  mysql:8.0

# 进入 MySQL 命令行
docker exec -it mysql8 mysql -uroot -proot123

# ==================== macOS 安装 ====================
brew install mysql
brew services start mysql
mysql_secure_installation   # 初始化安全设置

# ==================== Ubuntu 安装 ====================
sudo apt update
sudo apt install mysql-server
sudo systemctl start mysql
sudo mysql_secure_installation

# ==================== 验证安装 ====================
mysql -u root -p
# 输入密码后进入 MySQL 命令行

1.2 数据库和用户管理

sql
-- ==================== 数据库操作 ====================
CREATE DATABASE IF NOT EXISTS testdb
    DEFAULT CHARACTER SET utf8mb4
    DEFAULT COLLATE utf8mb4_unicode_ci;

SHOW DATABASES;
USE testdb;
DROP DATABASE IF EXISTS old_db;

-- ==================== 用户管理 ====================
-- 创建用户
CREATE USER 'gouser'@'%' IDENTIFIED BY 'gopass123';

-- 授权
GRANT ALL PRIVILEGES ON testdb.* TO 'gouser'@'%';
FLUSH PRIVILEGES;

-- 查看用户权限
SHOW GRANTS FOR 'gouser'@'%';

1.3 建表语句

sql
-- ==================== 用户表 ====================
CREATE TABLE IF NOT EXISTS users (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username   VARCHAR(50)  NOT NULL UNIQUE,
    email      VARCHAR(100) NOT NULL,
    password   VARCHAR(255) NOT NULL COMMENT '密码哈希',
    age        TINYINT UNSIGNED DEFAULT 0,
    balance    DECIMAL(10, 2) DEFAULT 0.00 COMMENT '账户余额',
    status     TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常 2=禁用',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_email (email),
    INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- ==================== 订单表 ====================
CREATE TABLE IF NOT EXISTS orders (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id    BIGINT UNSIGNED NOT NULL,
    product    VARCHAR(100) NOT NULL,
    amount     DECIMAL(10, 2) NOT NULL,
    quantity   INT UNSIGNED NOT NULL DEFAULT 1,
    status     TINYINT NOT NULL DEFAULT 0 COMMENT '0=待支付 1=已支付 2=已取消',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- ==================== 商品表 ====================
CREATE TABLE IF NOT EXISTS products (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name       VARCHAR(100) NOT NULL,
    price      DECIMAL(10, 2) NOT NULL,
    stock      INT UNSIGNED NOT NULL DEFAULT 0,
    category   VARCHAR(50) NOT NULL DEFAULT '',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

1.4 常用 CRUD 语句

sql
-- ==================== INSERT ====================
INSERT INTO users (username, email, password, age, balance)
VALUES ('alice', 'alice@test.com', 'hashed_pw', 25, 1000.00);

-- 批量插入
INSERT INTO users (username, email, password, age) VALUES
    ('bob', 'bob@test.com', 'hashed_pw', 30),
    ('charlie', 'charlie@test.com', 'hashed_pw', 28),
    ('dave', 'dave@test.com', 'hashed_pw', 35);

-- 插入或更新(唯一键冲突时更新)
INSERT INTO users (username, email, password)
VALUES ('alice', 'new@test.com', 'new_pw')
ON DUPLICATE KEY UPDATE email = VALUES(email);

-- ==================== SELECT ====================
-- 基本查询
SELECT id, username, email, age FROM users WHERE status = 1;

-- 条件查询
SELECT * FROM users
WHERE age BETWEEN 20 AND 30
  AND email LIKE '%@test.com'
  AND status IN (1, 2)
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;

-- 聚合查询
SELECT status, COUNT(*) AS cnt, AVG(age) AS avg_age
FROM users
GROUP BY status
HAVING cnt > 1;

-- ==================== UPDATE ====================
UPDATE users SET age = 26, balance = balance + 100
WHERE username = 'alice';

-- ==================== DELETE ====================
DELETE FROM users WHERE status = 2 AND created_at < '2024-01-01';

-- ==================== 常用函数 ====================
SELECT NOW();                          -- 当前时间
SELECT COUNT(*) FROM users;            -- 总行数
SELECT IFNULL(email, 'N/A') FROM users; -- 空值处理

1.5 Go 常用数据类型映射

text
MySQL 类型              Go 类型                 说明
──────────────────────────────────────────────────────────
TINYINT                 int8 / uint8            -128~127 / 0~255
INT                     int32                   约 ±21 亿
BIGINT                  int64                   约 ±922 京
BIGINT UNSIGNED         uint64                  0 ~ 1844 京
FLOAT                   float32                 精度有限
DOUBLE                  float64                 精度有限
DECIMAL(10,2)           string 或自定义类型      精确小数,用 string 最安全
VARCHAR / TEXT          string
DATETIME / TIMESTAMP    time.Time               需设置 parseTime=true
TINYINT (布尔)          bool                    0=false, 1=true
BLOB                    []byte
NULL 值                 sql.NullString 等        或用指针 *string

2. 官方 sql 接口及数据库的打开

2.1 database/sql 架构

text
Go 数据库访问的分层架构:

  你的代码


  database/sql          ← 标准接口(不含具体驱动)


  驱动(driver)         ← 第三方实现
    │                      github.com/go-sql-driver/mysql
    ▼                      github.com/lib/pq (PostgreSQL)
  MySQL / PostgreSQL       github.com/mattn/go-sqlite3

database/sql 的核心设计:
  - sql.DB 不是一个连接,而是一个连接池
  - 自动管理连接的创建、复用、回收
  - 线程安全,多个协程可以共享同一个 sql.DB
  - 通常整个应用只创建一个 sql.DB 实例

2.2 安装驱动和打开数据库

shell
# 安装 MySQL 驱动
go get github.com/go-sql-driver/mysql
go
package main

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql" // 空导入,只需要 init() 注册驱动
)

func main() {
    // ==================== DSN(Data Source Name)格式 ====================
    // user:password@tcp(host:port)/database?参数
    dsn := "gouser:gopass123@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=true&loc=Local"

    // DSN 参数说明:
    // charset=utf8mb4     使用 utf8mb4 编码(支持 emoji)
    // parseTime=true      自动将 DATETIME 解析为 time.Time
    // loc=Local           使用本地时区
    // timeout=5s          连接超时
    // readTimeout=10s     读超时
    // writeTimeout=10s    写超时
    // interpolateParams=true  客户端参数插值(减少一次网络往返)

    // ==================== 打开数据库 ====================
    // sql.Open 只是验证参数格式,不会真正连接数据库
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open 失败:", err) // 仅格式错误才会报错
        return
    }
    defer db.Close()

    // Ping 才会真正尝试连接
    if err := db.Ping(); err != nil {
        fmt.Println("连接失败:", err)
        return
    }
    fmt.Println("✅ 数据库连接成功")

    // ==================== 配置连接池 ====================
    db.SetMaxOpenConns(25)                  // 最大打开连接数
    db.SetMaxIdleConns(10)                  // 最大空闲连接数
    db.SetConnMaxLifetime(5 * time.Minute)  // 连接最大存活时间
    db.SetConnMaxIdleTime(3 * time.Minute)  // 空闲连接最大存活时间

    // ==================== 查看连接池状态 ====================
    stats := db.Stats()
    fmt.Printf("打开连接数: %d\n", stats.OpenConnections)
    fmt.Printf("使用中: %d\n", stats.InUse)
    fmt.Printf("空闲: %d\n", stats.Idle)
}

2.3 连接池最佳实践

text
连接池参数调优建议:

  MaxOpenConns(最大连接数):
    - 不要设太大,MySQL 默认最大连接 151
    - 建议:CPU 核数 * 2 + 磁盘数(经验公式)
    - Web 服务通常 20~50

  MaxIdleConns(最大空闲连接数):
    - 建议 = MaxOpenConns(避免频繁创建销毁连接)
    - 太小:频繁新建连接,增加延迟
    - 太大:浪费内存和 MySQL 连接资源

  ConnMaxLifetime(连接最大存活时间):
    - 必须 < MySQL 的 wait_timeout(默认 8 小时)
    - 建议 5~10 分钟
    - 防止使用已被 MySQL 服务端关闭的连接

  ConnMaxIdleTime(空闲连接过期时间):
    - 建议 3~5 分钟
    - 回收长期未使用的连接

2.4 封装数据库初始化

go
package db

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

// Config 数据库配置
type Config struct {
    Host     string
    Port     int
    User     string
    Password string
    DBName   string
}

// 全局数据库实例(整个应用共享)
var DB *sql.DB

// Init 初始化数据库连接
func Init(cfg Config) error {
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
        cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)

    var err error
    DB, err = sql.Open("mysql", dsn)
    if err != nil {
        return fmt.Errorf("sql.Open: %w", err)
    }

    DB.SetMaxOpenConns(25)
    DB.SetMaxIdleConns(25)
    DB.SetConnMaxLifetime(5 * time.Minute)

    if err := DB.Ping(); err != nil {
        return fmt.Errorf("db.Ping: %w", err)
    }

    return nil
}

// Close 关闭数据库连接
func Close() error {
    if DB != nil {
        return DB.Close()
    }
    return nil
}

3. 增删改查和事务

3.1 数据模型

go
package main

import (
    "database/sql"
    "time"
)

// User 对应 users 表
type User struct {
    ID        int64
    Username  string
    Email     string
    Password  string
    Age       int
    Balance   float64
    Status    int
    CreatedAt time.Time
    UpdatedAt time.Time
}

// Order 对应 orders 表
type Order struct {
    ID        int64
    UserID    int64
    Product   string
    Amount    float64
    Quantity  int
    Status    int
    CreatedAt time.Time
}

3.2 插入(INSERT)

go
package main

import (
    "database/sql"
    "fmt"
)

// CreateUser 插入一条用户记录
func CreateUser(db *sql.DB, user *User) (int64, error) {
    // Exec 用于不返回数据的 SQL(INSERT、UPDATE、DELETE)
    // 使用 ? 占位符防止 SQL 注入
    result, err := db.Exec(
        `INSERT INTO users (username, email, password, age, balance, status)
         VALUES (?, ?, ?, ?, ?, ?)`,
        user.Username, user.Email, user.Password,
        user.Age, user.Balance, user.Status,
    )
    if err != nil {
        return 0, fmt.Errorf("insert user: %w", err)
    }

    // 获取自增 ID
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("last insert id: %w", err)
    }

    // 获取受影响的行数
    rows, _ := result.RowsAffected()
    fmt.Printf("插入成功,ID=%d,影响 %d\n", id, rows)

    return id, nil
}

// 使用示例
func insertExample(db *sql.DB) {
    user := &User{
        Username: "alice",
        Email:    "alice@example.com",
        Password: "hashed_password",
        Age:      25,
        Balance:  1000.00,
        Status:   1,
    }

    id, err := CreateUser(db, user)
    if err != nil {
        fmt.Println("创建失败:", err)
        return
    }
    fmt.Println("新用户 ID:", id)
}

❌ 永远不要拼接 SQL 字符串!

go
// ❌ 极度危险:SQL 注入
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", userInput)
// 如果 userInput = "'; DROP TABLE users; --"
// 整个表就没了

// ✅ 始终使用参数化查询
db.Query("SELECT * FROM users WHERE username = ?", userInput)

3.3 查询(SELECT)

go
package main

import (
    "database/sql"
    "fmt"
)

// ==================== 查询单行:QueryRow ====================

// GetUserByID 根据 ID 查询用户
func GetUserByID(db *sql.DB, id int64) (*User, error) {
    user := &User{}

    // QueryRow 只返回一行,如果没有找到返回 sql.ErrNoRows
    err := db.QueryRow(
        `SELECT id, username, email, age, balance, status, created_at
         FROM users WHERE id = ?`, id,
    ).Scan(
        &user.ID, &user.Username, &user.Email,
        &user.Age, &user.Balance, &user.Status,
        &user.CreatedAt,
    )

    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("user %d not found", id)
        }
        return nil, fmt.Errorf("query user: %w", err)
    }

    return user, nil
}

// ==================== 查询多行:Query ====================

// ListUsers 查询用户列表
func ListUsers(db *sql.DB, status int) ([]*User, error) {
    rows, err := db.Query(
        `SELECT id, username, email, age, balance, status, created_at
         FROM users WHERE status = ? ORDER BY id DESC`, status,
    )
    if err != nil {
        return nil, fmt.Errorf("query users: %w", err)
    }
    defer rows.Close() // 必须关闭!否则连接不会归还到连接池

    var users []*User
    for rows.Next() {
        user := &User{}
        err := rows.Scan(
            &user.ID, &user.Username, &user.Email,
            &user.Age, &user.Balance, &user.Status,
            &user.CreatedAt,
        )
        if err != nil {
            return nil, fmt.Errorf("scan user: %w", err)
        }
        users = append(users, user)
    }

    // 检查迭代过程中是否有错误
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("rows iteration: %w", err)
    }

    return users, nil
}

// ==================== 处理 NULL 值 ====================

func queryWithNull(db *sql.DB) {
    var (
        id    int64
        name  string
        email sql.NullString // 可能为 NULL 的字段
        age   sql.NullInt64
    )

    err := db.QueryRow("SELECT id, username, email, age FROM users WHERE id = ?", 1).
        Scan(&id, &name, &email, &age)
    if err != nil {
        return
    }

    // 检查是否为 NULL
    if email.Valid {
        fmt.Println("email:", email.String)
    } else {
        fmt.Println("email: NULL")
    }

    // 或者用指针(更简洁)
    var emailPtr *string
    db.QueryRow("SELECT email FROM users WHERE id = ?", 1).Scan(&emailPtr)
    if emailPtr != nil {
        fmt.Println("email:", *emailPtr)
    }
}

3.4 更新(UPDATE)和删除(DELETE)

go
package main

import (
    "database/sql"
    "fmt"
)

// UpdateUserAge 更新用户年龄
func UpdateUserAge(db *sql.DB, id int64, age int) (int64, error) {
    result, err := db.Exec(
        "UPDATE users SET age = ? WHERE id = ?",
        age, id,
    )
    if err != nil {
        return 0, fmt.Errorf("update user: %w", err)
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        return 0, fmt.Errorf("user %d not found", id)
    }
    return affected, nil
}

// DeleteUser 删除用户
func DeleteUser(db *sql.DB, id int64) error {
    result, err := db.Exec("DELETE FROM users WHERE id = ?", id)
    if err != nil {
        return fmt.Errorf("delete user: %w", err)
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        return fmt.Errorf("user %d not found", id)
    }
    fmt.Printf("删除了 %d\n", affected)
    return nil
}

3.5 事务(Transaction)

go
package main

import (
    "context"
    "database/sql"
    "fmt"
)

// Transfer 转账:从 A 账户转给 B 账户
// 必须用事务保证原子性(要么都成功,要么都回滚)
func Transfer(db *sql.DB, fromID, toID int64, amount float64) error {
    // 开始事务
    tx, err := db.Begin()
    if err != nil {
        return fmt.Errorf("begin tx: %w", err)
    }

    // defer 回滚:如果函数正常提交了,Rollback 是无操作
    // 如果中途 return err,Rollback 会回滚所有操作
    defer tx.Rollback()

    // 1. 查询转出方余额(用 FOR UPDATE 加行锁,防止并发问题)
    var balance float64
    err = tx.QueryRow(
        "SELECT balance FROM users WHERE id = ? FOR UPDATE", fromID,
    ).Scan(&balance)
    if err != nil {
        return fmt.Errorf("query from user: %w", err)
    }
    if balance < amount {
        return fmt.Errorf("余额不足: %.2f < %.2f", balance, amount)
    }

    // 2. 扣款
    _, err = tx.Exec("UPDATE users SET balance = balance - ? WHERE id = ?",
        amount, fromID)
    if err != nil {
        return fmt.Errorf("deduct: %w", err)
    }

    // 3. 到账
    _, err = tx.Exec("UPDATE users SET balance = balance + ? WHERE id = ?",
        amount, toID)
    if err != nil {
        return fmt.Errorf("credit: %w", err)
    }

    // 4. 记录订单
    _, err = tx.Exec(
        `INSERT INTO orders (user_id, product, amount, status)
         VALUES (?, ?, ?, 1)`,
        fromID, fmt.Sprintf("转账给用户%d", toID), amount,
    )
    if err != nil {
        return fmt.Errorf("insert order: %w", err)
    }

    // 5. 提交事务
    if err := tx.Commit(); err != nil {
        return fmt.Errorf("commit: %w", err)
    }

    fmt.Printf("✅ 转账成功: 用户%d → 用户%d, 金额 %.2f\n", fromID, toID, amount)
    return nil
}

// ==================== 带 context 的事务 ====================

func TransferWithContext(ctx context.Context, db *sql.DB, fromID, toID int64, amount float64) error {
    // 带超时的事务
    tx, err := db.BeginTx(ctx, &sql.TxOptions{
        Isolation: sql.LevelReadCommitted, // 隔离级别
        ReadOnly:  false,
    })
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // 使用 tx.ExecContext / tx.QueryRowContext 传递 context
    _, err = tx.ExecContext(ctx,
        "UPDATE users SET balance = balance - ? WHERE id = ?",
        amount, fromID)
    if err != nil {
        return err
    }

    _, err = tx.ExecContext(ctx,
        "UPDATE users SET balance = balance + ? WHERE id = ?",
        amount, toID)
    if err != nil {
        return err
    }

    return tx.Commit()
}

// ==================== 通用事务封装 ====================

// WithTransaction 事务执行器:自动 Commit/Rollback
func WithTransaction(db *sql.DB, fn func(tx *sql.Tx) error) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()

    if err := fn(tx); err != nil {
        return err
    }

    return tx.Commit()
}

// 使用
func example(db *sql.DB) {
    err := WithTransaction(db, func(tx *sql.Tx) error {
        // 在这里写所有事务操作
        _, err := tx.Exec("UPDATE users SET age = 30 WHERE id = 1")
        if err != nil {
            return err // 自动回滚
        }
        _, err = tx.Exec("UPDATE users SET age = 25 WHERE id = 2")
        return err // 返回 nil 则自动提交
    })
    if err != nil {
        fmt.Println("事务失败:", err)
    }
}

⚠️ 事务注意事项

  • 事务内的所有操作必须用 tx.Exec / tx.Query,不能用 db.Exec(否则不在事务内)
  • defer tx.Rollback() 是安全的:已 Commit 的事务,Rollback 是无操作
  • 事务要尽量短,长事务会锁定行,降低并发性能
  • FOR UPDATE 会加行锁,防止并发读写冲突

4. SQL Builder

4.1 为什么需要 SQL Builder

text
手写 SQL 的痛点:
  1. 条件拼接繁琐(WHERE 条件动态组合)
  2. 容易写错(多余的 AND、漏逗号)
  3. 参数对应容易错位
  4. 不同数据库语法不同

SQL Builder 的作用:
  用 Go 代码构建 SQL,类型安全,自动处理占位符和参数

常用 SQL Builder:
  - squirrel:轻量级,API 简洁(本节使用)
  - goqu:功能丰富
  - sqlc:从 SQL 生成 Go 代码(另一种思路)

4.2 squirrel 基本用法

shell
go get github.com/Masterminds/squirrel
go
package main

import (
    "database/sql"
    "fmt"
    "strings"

    sq "github.com/Masterminds/squirrel"
)

func main() {
    // ==================== SELECT ====================
    query, args, err := sq.Select("id", "username", "email", "age").
        From("users").
        Where(sq.Eq{"status": 1}).
        Where(sq.GtOrEq{"age": 18}).
        OrderBy("created_at DESC").
        Limit(10).
        Offset(0).
        ToSql()

    fmt.Println("SQL:", query)
    fmt.Println("Args:", args)
    // SQL: SELECT id, username, email, age FROM users WHERE status = ? AND age >= ? ORDER BY created_at DESC LIMIT 10 OFFSET 0
    // Args: [1 18]

    // ==================== INSERT ====================
    query, args, _ = sq.Insert("users").
        Columns("username", "email", "password", "age").
        Values("alice", "alice@test.com", "hashed", 25).
        Values("bob", "bob@test.com", "hashed", 30). // 批量插入
        ToSql()

    fmt.Println("\nSQL:", query)
    fmt.Println("Args:", args)

    // ==================== UPDATE ====================
    query, args, _ = sq.Update("users").
        Set("age", 26).
        Set("email", "new@test.com").
        Where(sq.Eq{"id": 1}).
        ToSql()

    fmt.Println("\nSQL:", query)
    fmt.Println("Args:", args)

    // ==================== DELETE ====================
    query, args, _ = sq.Delete("users").
        Where(sq.And{
            sq.Eq{"status": 2},
            sq.Lt{"created_at": "2024-01-01"},
        }).
        ToSql()

    fmt.Println("\nSQL:", query)
    fmt.Println("Args:", args)

    // ==================== 复杂条件 ====================
    query, args, _ = sq.Select("*").From("users").
        Where(sq.Or{
            sq.Eq{"username": "alice"},
            sq.And{
                sq.GtOrEq{"age": 20},
                sq.LtOrEq{"age": 30},
            },
        }).
        ToSql()

    fmt.Println("\nSQL:", query)
    // SELECT * FROM users WHERE (username = ? OR (age >= ? AND age <= ?))

    // ==================== LIKE 查询 ====================
    query, args, _ = sq.Select("*").From("users").
        Where(sq.Like{"email": "%@test.com"}).
        ToSql()

    fmt.Println("\nSQL:", query)

    // ==================== JOIN ====================
    query, args, _ = sq.Select("u.username", "o.product", "o.amount").
        From("users u").
        Join("orders o ON u.id = o.user_id").
        Where(sq.Eq{"o.status": 1}).
        ToSql()

    fmt.Println("\nSQL:", query)
}

4.3 动态条件构建

go
package main

import (
    "database/sql"
    "fmt"

    sq "github.com/Masterminds/squirrel"
)

// UserFilter 用户查询过滤条件
type UserFilter struct {
    Username *string
    Email    *string
    MinAge   *int
    MaxAge   *int
    Status   *int
    Page     int
    PageSize int
}

// ListUsersWithFilter 根据动态条件查询用户
func ListUsersWithFilter(db *sql.DB, filter UserFilter) ([]*User, int64, error) {
    // 基础查询
    builder := sq.Select("id", "username", "email", "age", "balance", "status", "created_at").
        From("users")

    // 动态添加条件(只有传了的条件才加上)
    if filter.Username != nil {
        builder = builder.Where(sq.Like{"username": "%" + *filter.Username + "%"})
    }
    if filter.Email != nil {
        builder = builder.Where(sq.Like{"email": "%" + *filter.Email + "%"})
    }
    if filter.MinAge != nil {
        builder = builder.Where(sq.GtOrEq{"age": *filter.MinAge})
    }
    if filter.MaxAge != nil {
        builder = builder.Where(sq.LtOrEq{"age": *filter.MaxAge})
    }
    if filter.Status != nil {
        builder = builder.Where(sq.Eq{"status": *filter.Status})
    }

    // 总数查询(用同样的 WHERE 条件)
    countBuilder := sq.Select("COUNT(*)").From("users")
    // 复用 WHERE 条件需要手动构建,这里简化为独立查询
    var total int64

    // 分页
    if filter.PageSize <= 0 {
        filter.PageSize = 20
    }
    if filter.Page <= 0 {
        filter.Page = 1
    }
    offset := uint64((filter.Page - 1) * filter.PageSize)
    builder = builder.
        OrderBy("id DESC").
        Limit(uint64(filter.PageSize)).
        Offset(offset)

    // 生成 SQL
    query, args, err := builder.ToSql()
    if err != nil {
        return nil, 0, fmt.Errorf("build sql: %w", err)
    }

    fmt.Printf("生成的 SQL: %s\n", query)
    fmt.Printf("参数: %v\n", args)

    // 执行查询
    rows, err := db.Query(query, args...)
    if err != nil {
        return nil, 0, err
    }
    defer rows.Close()

    var users []*User
    for rows.Next() {
        u := &User{}
        rows.Scan(&u.ID, &u.Username, &u.Email, &u.Age, &u.Balance, &u.Status, &u.CreatedAt)
        users = append(users, u)
    }

    return users, total, nil
}

// 使用示例
func filterExample(db *sql.DB) {
    username := "ali"
    minAge := 20
    status := 1

    users, total, _ := ListUsersWithFilter(db, UserFilter{
        Username: &username,
        MinAge:   &minAge,
        Status:   &status,
        Page:     1,
        PageSize: 10,
    })

    fmt.Printf("共 %d 条,当前页 %d\n", total, len(users))
}

5. 练习:用 stmt 批量插入数据

💡 要求对比三种批量插入方式的性能:逐条插入、Prepared Statement 批量插入、拼接 VALUES 批量插入。插入 10000 条记录,对比耗时。

go
package main

import (
    "database/sql"
    "fmt"
    "strings"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

// ==================== 方式1:逐条插入(最慢)====================
func insertOneByOne(db *sql.DB, count int) time.Duration {
    start := time.Now()

    for i := 0; i < count; i++ {
        db.Exec(
            `INSERT INTO users (username, email, password, age, status)
             VALUES (?, ?, ?, ?, ?)`,
            fmt.Sprintf("user_one_%d", i),
            fmt.Sprintf("one_%d@test.com", i),
            "hashed_pw",
            20+i%50,
            1,
        )
    }

    return time.Since(start)
}

// ==================== 方式2:Prepared Statement(推荐)====================
// 预编译 SQL 只解析一次,后续执行只传参数
// 减少 SQL 解析开销,但仍然是逐条网络往返
func insertWithStmt(db *sql.DB, count int) time.Duration {
    start := time.Now()

    // 预编译 SQL(只解析一次)
    stmt, err := db.Prepare(
        `INSERT INTO users (username, email, password, age, status)
         VALUES (?, ?, ?, ?, ?)`,
    )
    if err != nil {
        fmt.Println("Prepare 失败:", err)
        return 0
    }
    defer stmt.Close()

    for i := 0; i < count; i++ {
        // 每次执行只传参数,不需要重新解析 SQL
        _, err := stmt.Exec(
            fmt.Sprintf("user_stmt_%d", i),
            fmt.Sprintf("stmt_%d@test.com", i),
            "hashed_pw",
            20+i%50,
            1,
        )
        if err != nil {
            fmt.Println("Exec 失败:", err)
        }
    }

    return time.Since(start)
}

// ==================== 方式3:拼接 VALUES 批量插入(最快)====================
// INSERT INTO users (...) VALUES (...), (...), (...), ...
// 一条 SQL 插入多行,大幅减少网络往返
func insertBatch(db *sql.DB, count int, batchSize int) time.Duration {
    start := time.Now()

    for i := 0; i < count; i += batchSize {
        // 计算本批数量
        end := i + batchSize
        if end > count {
            end = count
        }
        n := end - i

        // 构建 VALUES 占位符
        placeholders := make([]string, n)
        args := make([]interface{}, 0, n*5)

        for j := 0; j < n; j++ {
            idx := i + j
            placeholders[j] = "(?, ?, ?, ?, ?)"
            args = append(args,
                fmt.Sprintf("user_batch_%d", idx),
                fmt.Sprintf("batch_%d@test.com", idx),
                "hashed_pw",
                20+idx%50,
                1,
            )
        }

        query := fmt.Sprintf(
            `INSERT INTO users (username, email, password, age, status) VALUES %s`,
            strings.Join(placeholders, ","),
        )

        _, err := db.Exec(query, args...)
        if err != nil {
            fmt.Println("批量插入失败:", err)
        }
    }

    return time.Since(start)
}

// ==================== 方式4:事务 + Prepared Statement ====================
// 在事务中使用 Stmt,减少自动提交开销
func insertWithTxStmt(db *sql.DB, count int) time.Duration {
    start := time.Now()

    tx, err := db.Begin()
    if err != nil {
        return 0
    }
    defer tx.Rollback()

    stmt, err := tx.Prepare(
        `INSERT INTO users (username, email, password, age, status)
         VALUES (?, ?, ?, ?, ?)`,
    )
    if err != nil {
        return 0
    }
    defer stmt.Close()

    for i := 0; i < count; i++ {
        _, err := stmt.Exec(
            fmt.Sprintf("user_txstmt_%d", i),
            fmt.Sprintf("txstmt_%d@test.com", i),
            "hashed_pw",
            20+i%50,
            1,
        )
        if err != nil {
            fmt.Println("Exec 失败:", err)
            return time.Since(start)
        }
    }

    tx.Commit()
    return time.Since(start)
}

func main() {
    dsn := "gouser:gopass123@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=true"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("连接失败:", err)
        return
    }
    defer db.Close()

    count := 10000
    fmt.Printf("插入 %d 条记录性能对比\n\n", count)

    // 每次测试前清空表
    cleanup := func() {
        db.Exec("TRUNCATE TABLE users")
    }

    // 方式1:逐条插入
    cleanup()
    d1 := insertOneByOne(db, count)
    fmt.Printf("方式1 逐条插入:         %s\n", d1)

    // 方式2:Prepared Statement
    cleanup()
    d2 := insertWithStmt(db, count)
    fmt.Printf("方式2 Prepared Stmt:    %s\n", d2)

    // 方式3:批量 VALUES(每批 500 条)
    cleanup()
    d3 := insertBatch(db, count, 500)
    fmt.Printf("方式3 批量 VALUES(500): %s\n", d3)

    // 方式4:事务 + Stmt
    cleanup()
    d4 := insertWithTxStmt(db, count)
    fmt.Printf("方式4 事务 + Stmt:      %s\n", d4)

    // 典型结果(仅供参考,具体取决于硬件和网络):
    // 方式1 逐条插入:         12.5s
    // 方式2 Prepared Stmt:    8.3s
    // 方式3 批量 VALUES(500): 0.4s    ← 最快(减少网络往返)
    // 方式4 事务 + Stmt:      1.2s

    fmt.Println("\n结论:批量 VALUES 拼接最快,事务+Stmt 次之")
}
text
Prepared Statement 工作原理:

  普通 Exec(每次都要解析):
    客户端 → "INSERT INTO users ... VALUES('alice',...)" → MySQL 解析+执行

  Prepared Statement(只解析一次):
    第1步:客户端 → "INSERT INTO users ... VALUES(?,?,...)" → MySQL 解析,返回 stmt_id
    第2步:客户端 → stmt_id + ['alice', ...] → MySQL 直接执行(跳过解析)
    第3步:客户端 → stmt_id + ['bob', ...]   → MySQL 直接执行
    ...

  优点:
    ✅ SQL 只解析一次
    ✅ 防止 SQL 注入(参数和 SQL 分离)
    ✅ 减少网络传输量(不重复发送 SQL 文本)

  缺点:
    ⚠️ 仍然是逐条网络往返(Exec 一次 = 一次网络请求)
    ⚠️ 需要两次网络往返来 Prepare(创建 + 关闭)

6. 多表联合查询和翻页查询

6.1 JOIN 查询

go
package main

import (
    "database/sql"
    "fmt"
    "time"
)

// UserOrder 联合查询结果
type UserOrder struct {
    UserID    int64
    Username  string
    OrderID   int64
    Product   string
    Amount    float64
    Quantity  int
    OrderTime time.Time
}

// ==================== INNER JOIN ====================
// 只返回两表都匹配的行

func GetUserOrders(db *sql.DB, userID int64) ([]*UserOrder, error) {
    rows, err := db.Query(`
        SELECT u.id, u.username, o.id, o.product, o.amount, o.quantity, o.created_at
        FROM users u
        INNER JOIN orders o ON u.id = o.user_id
        WHERE u.id = ? AND o.status = 1
        ORDER BY o.created_at DESC`,
        userID,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var results []*UserOrder
    for rows.Next() {
        r := &UserOrder{}
        rows.Scan(&r.UserID, &r.Username, &r.OrderID,
            &r.Product, &r.Amount, &r.Quantity, &r.OrderTime)
        results = append(results, r)
    }
    return results, rows.Err()
}

// ==================== LEFT JOIN ====================
// 返回左表所有行,右表没有匹配则为 NULL

// UserWithOrderCount 用户及其订单统计
type UserWithOrderCount struct {
    UserID     int64
    Username   string
    OrderCount int
    TotalSpent float64
}

func GetUsersWithOrderStats(db *sql.DB) ([]*UserWithOrderCount, error) {
    rows, err := db.Query(`
        SELECT u.id, u.username,
               COUNT(o.id) AS order_count,
               COALESCE(SUM(o.amount * o.quantity), 0) AS total_spent
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id AND o.status = 1
        GROUP BY u.id, u.username
        ORDER BY total_spent DESC`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var results []*UserWithOrderCount
    for rows.Next() {
        r := &UserWithOrderCount{}
        rows.Scan(&r.UserID, &r.Username, &r.OrderCount, &r.TotalSpent)
        results = append(results, r)
    }
    return results, rows.Err()
}

// ==================== 子查询 ====================

// 查询消费超过平均值的用户
func GetHighSpenders(db *sql.DB) ([]*UserWithOrderCount, error) {
    rows, err := db.Query(`
        SELECT u.id, u.username, sub.order_count, sub.total_spent
        FROM users u
        INNER JOIN (
            SELECT user_id,
                   COUNT(*) AS order_count,
                   SUM(amount * quantity) AS total_spent
            FROM orders
            WHERE status = 1
            GROUP BY user_id
        ) sub ON u.id = sub.user_id
        WHERE sub.total_spent > (
            SELECT AVG(amount * quantity) FROM orders WHERE status = 1
        )
        ORDER BY sub.total_spent DESC`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var results []*UserWithOrderCount
    for rows.Next() {
        r := &UserWithOrderCount{}
        rows.Scan(&r.UserID, &r.Username, &r.OrderCount, &r.TotalSpent)
        results = append(results, r)
    }
    return results, rows.Err()
}

6.2 翻页查询

go
package main

import (
    "database/sql"
    "fmt"
    "math"
)

// PageResult 分页结果
type PageResult[T any] struct {
    Items      []T   `json:"items"`
    Total      int64 `json:"total"`       // 总记录数
    Page       int   `json:"page"`        // 当前页码
    PageSize   int   `json:"page_size"`   // 每页大小
    TotalPages int   `json:"total_pages"` // 总页数
}

// ==================== 方式1:LIMIT + OFFSET(最常用)====================
// 简单但大偏移量时性能差(MySQL 要扫描 OFFSET 行再丢弃)

func ListUsersPage(db *sql.DB, page, pageSize int) (*PageResult[User], error) {
    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 20
    }
    offset := (page - 1) * pageSize

    // 查询总数
    var total int64
    err := db.QueryRow("SELECT COUNT(*) FROM users WHERE status = 1").Scan(&total)
    if err != nil {
        return nil, fmt.Errorf("count: %w", err)
    }

    // 查询当前页数据
    rows, err := db.Query(`
        SELECT id, username, email, age, balance, status, created_at
        FROM users
        WHERE status = 1
        ORDER BY id DESC
        LIMIT ? OFFSET ?`,
        pageSize, offset,
    )
    if err != nil {
        return nil, fmt.Errorf("query: %w", err)
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        rows.Scan(&u.ID, &u.Username, &u.Email, &u.Age,
            &u.Balance, &u.Status, &u.CreatedAt)
        users = append(users, u)
    }

    return &PageResult[User]{
        Items:      users,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: int(math.Ceil(float64(total) / float64(pageSize))),
    }, nil
}

// ==================== 方式2:游标分页(大数据量推荐)====================
// 基于上一页最后一条记录的 ID 翻页
// 不需要 OFFSET,性能恒定

func ListUsersCursor(db *sql.DB, lastID int64, pageSize int) ([]User, error) {
    if pageSize < 1 || pageSize > 100 {
        pageSize = 20
    }

    var rows *sql.Rows
    var err error

    if lastID == 0 {
        // 第一页
        rows, err = db.Query(`
            SELECT id, username, email, age, balance, status, created_at
            FROM users
            WHERE status = 1
            ORDER BY id DESC
            LIMIT ?`,
            pageSize,
        )
    } else {
        // 后续页:只查询 ID 小于 lastID 的记录
        rows, err = db.Query(`
            SELECT id, username, email, age, balance, status, created_at
            FROM users
            WHERE status = 1 AND id < ?
            ORDER BY id DESC
            LIMIT ?`,
            lastID, pageSize,
        )
    }

    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        rows.Scan(&u.ID, &u.Username, &u.Email, &u.Age,
            &u.Balance, &u.Status, &u.CreatedAt)
        users = append(users, u)
    }
    return users, rows.Err()
}

// ==================== 方式3:延迟关联(优化大偏移量)====================
// 先查 ID,再回表查数据
// 适用于 OFFSET 很大但需要使用 LIMIT+OFFSET 的场景

func ListUsersDeferred(db *sql.DB, page, pageSize int) ([]User, error) {
    offset := (page - 1) * pageSize

    // 子查询只扫描主键索引,速度快
    rows, err := db.Query(`
        SELECT u.id, u.username, u.email, u.age, u.balance, u.status, u.created_at
        FROM users u
        INNER JOIN (
            SELECT id FROM users
            WHERE status = 1
            ORDER BY id DESC
            LIMIT ? OFFSET ?
        ) tmp ON u.id = tmp.id
        ORDER BY u.id DESC`,
        pageSize, offset,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        rows.Scan(&u.ID, &u.Username, &u.Email, &u.Age,
            &u.Balance, &u.Status, &u.CreatedAt)
        users = append(users, u)
    }
    return users, rows.Err()
}

func main() {
    // 分页方式对比
    fmt.Println(`
分页方式对比:
───────────────────────────────────────────────────────────────
方式                性能              适用场景
───────────────────────────────────────────────────────────────
LIMIT + OFFSET     小偏移快,大偏移慢  数据量 < 10 万,页数不深
游标分页            恒定快              无限滚动、瀑布流、API 分页
延迟关联            中等,优于朴素偏移   必须用 OFFSET 但数据量大
───────────────────────────────────────────────────────────────

LIMIT + OFFSET 为什么慢?
  SELECT * FROM users ORDER BY id DESC LIMIT 20 OFFSET 100000
  MySQL 实际做了:扫描 100020 行 → 丢弃前 100000 行 → 返回 20 行

游标分页为什么快?
  SELECT * FROM users WHERE id < 99999 ORDER BY id DESC LIMIT 20
  MySQL 直接从索引定位到 id < 99999 → 返回 20 行
  无论翻到第几页,耗时恒定`)
}

6.3 综合示例:带排序和搜索的翻页

go
package main

import (
    "database/sql"
    "fmt"

    sq "github.com/Masterminds/squirrel"
)

// SearchParams 搜索参数
type SearchParams struct {
    Keyword  string // 搜索关键词(模糊匹配用户名或邮箱)
    MinAge   int
    MaxAge   int
    Status   int
    SortBy   string // 排序字段:id, username, age, created_at
    SortDir  string // 排序方向:ASC, DESC
    Page     int
    PageSize int
}

// SearchUsersWithOrders 搜索用户及其订单汇总
func SearchUsersWithOrders(db *sql.DB, params SearchParams) (*PageResult[UserWithOrderCount], error) {
    // 设置默认值
    if params.PageSize <= 0 || params.PageSize > 100 {
        params.PageSize = 20
    }
    if params.Page <= 0 {
        params.Page = 1
    }
    if params.SortBy == "" {
        params.SortBy = "u.id"
    }
    if params.SortDir == "" {
        params.SortDir = "DESC"
    }

    // 验证排序字段(防注入)
    allowedSorts := map[string]string{
        "id":         "u.id",
        "username":   "u.username",
        "age":        "u.age",
        "created_at": "u.created_at",
        "total":      "total_spent",
    }
    sortCol, ok := allowedSorts[params.SortBy]
    if !ok {
        sortCol = "u.id"
    }
    if params.SortDir != "ASC" && params.SortDir != "DESC" {
        params.SortDir = "DESC"
    }

    // 构建 WHERE 条件
    var conditions []sq.Sqlizer
    conditions = append(conditions, sq.Eq{"u.status": params.Status})

    if params.Keyword != "" {
        conditions = append(conditions, sq.Or{
            sq.Like{"u.username": "%" + params.Keyword + "%"},
            sq.Like{"u.email": "%" + params.Keyword + "%"},
        })
    }
    if params.MinAge > 0 {
        conditions = append(conditions, sq.GtOrEq{"u.age": params.MinAge})
    }
    if params.MaxAge > 0 {
        conditions = append(conditions, sq.LtOrEq{"u.age": params.MaxAge})
    }

    where := sq.And(conditions)

    // COUNT 查询
    countQuery, countArgs, _ := sq.Select("COUNT(DISTINCT u.id)").
        From("users u").
        Where(where).
        ToSql()

    var total int64
    db.QueryRow(countQuery, countArgs...).Scan(&total)

    // 数据查询
    offset := uint64((params.Page - 1) * params.PageSize)

    dataQuery, dataArgs, _ := sq.Select(
        "u.id", "u.username",
        "COUNT(o.id) AS order_count",
        "COALESCE(SUM(o.amount * o.quantity), 0) AS total_spent",
    ).
        From("users u").
        LeftJoin("orders o ON u.id = o.user_id AND o.status = 1").
        Where(where).
        GroupBy("u.id", "u.username").
        OrderBy(sortCol + " " + params.SortDir).
        Limit(uint64(params.PageSize)).
        Offset(offset).
        ToSql()

    fmt.Printf("SQL: %s\nArgs: %v\n\n", dataQuery, dataArgs)

    rows, err := db.Query(dataQuery, dataArgs...)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var items []UserWithOrderCount
    for rows.Next() {
        var r UserWithOrderCount
        rows.Scan(&r.UserID, &r.Username, &r.OrderCount, &r.TotalSpent)
        items = append(items, r)
    }

    totalPages := int((total + int64(params.PageSize) - 1) / int64(params.PageSize))

    return &PageResult[UserWithOrderCount]{
        Items:      items,
        Total:      total,
        Page:       params.Page,
        PageSize:   params.PageSize,
        TotalPages: totalPages,
    }, nil
}

// 使用示例
func searchExample(db *sql.DB) {
    result, err := SearchUsersWithOrders(db, SearchParams{
        Keyword:  "ali",
        MinAge:   20,
        Status:   1,
        SortBy:   "total",
        SortDir:  "DESC",
        Page:     1,
        PageSize: 10,
    })
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    fmt.Printf("共 %d 条,第 %d/%d\n", result.Total, result.Page, result.TotalPages)
    for _, u := range result.Items {
        fmt.Printf("  %-10s 订单数: %d, 总消费: %.2f\n",
            u.Username, u.OrderCount, u.TotalSpent)
    }
}

附录:database/sql 常用 API 速查

text
方法                                  说明
──────────────────────────────────────────────────────────────
sql.Open(driver, dsn)                创建连接池(不连接数据库)
db.Ping()                            验证连接是否可用
db.Close()                           关闭连接池
db.Exec(query, args...)              执行不返回行的 SQL(INSERT/UPDATE/DELETE)
db.Query(query, args...)             执行返回多行的 SQL(SELECT)
db.QueryRow(query, args...)          执行返回单行的 SQL
db.Prepare(query)                    预编译 SQL,返回 *Stmt
db.Begin()                           开始事务,返回 *Tx
db.BeginTx(ctx, opts)                带选项的事务
db.SetMaxOpenConns(n)                设置最大连接数
db.SetMaxIdleConns(n)                设置最大空闲连接数
db.Stats()                           查看连接池统计

rows.Next()                          移动到下一行
rows.Scan(dest...)                   读取当前行的列值
rows.Close()                         关闭结果集(释放连接)
rows.Err()                           获取迭代错误

row.Scan(dest...)                    读取单行结果

result.LastInsertId()                获取自增 ID
result.RowsAffected()               获取受影响行数

tx.Exec / tx.Query / tx.QueryRow    事务中执行 SQL
tx.Commit()                          提交事务
tx.Rollback()                        回滚事务

stmt.Exec(args...)                   执行预编译 SQL
stmt.Query(args...)                  查询预编译 SQL
stmt.Close()                         关闭预编译语句