← 返回目录

第六章:数据库

database/sql 接口、MySQL 与事务

1 database/sql 接口

Go 的 database/sql 包定义了一套通用数据库接口,具体的数据库驱动通过匿名导入注册。这种设计使得切换数据库只需更换驱动,业务代码几乎无需修改。

连接数据库

package main

import (
    "database/sql"
    "fmt"
    "log"

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

func main() {
    // DSN 格式: user:password@tcp(host:port)/dbname?params
    dsn := "root:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal("打开数据库失败:", err)
    }
    defer db.Close()

    // sql.Open 不会真正建立连接,需要 Ping 验证
    if err := db.Ping(); err != nil {
        log.Fatal("连接数据库失败:", err)
    }
    fmt.Println("数据库连接成功!")
}

匿名导入:_ "github.com/go-sql-driver/mysql" 中的下划线 _ 表示只执行包的 init() 函数来注册驱动,不直接使用包中的符号。

安装 MySQL 驱动

go get -u github.com/go-sql-driver/mysql

🔄 Go database/sql vs PHP PDO vs Node.js mysql2

// Go:统一接口 + 驱动匿名导入
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", dsn)
# PHP PDO:统一接口 + DSN
# $pdo = new PDO("mysql:host=127.0.0.1;dbname=myapp", "root", "password");

# Node.js mysql2:专用库
# const mysql = require('mysql2/promise');
# const conn = await mysql.createConnection({host: '127.0.0.1', ...});

相似点:Go 的 database/sql 类似 PHP 的 PDO——都提供统一抽象接口,支持多种数据库后端。

2 CRUD 操作

查询多行 — db.Query

func getUsers(db *sql.DB) ([]User, error) {
    rows, err := db.Query("SELECT id, name, email FROM users WHERE active = ?", true)
    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); err != nil {
            return nil, err
        }
        users = append(users, u)
    }

    // 检查遍历过程中是否发生错误
    if err := rows.Err(); err != nil {
        return nil, err
    }
    return users, nil
}

查询单行 — db.QueryRow

func getUserByID(db *sql.DB, id int) (*User, error) {
    var u User
    err := db.QueryRow(
        "SELECT id, name, email FROM users WHERE id = ?", id,
    ).Scan(&u.ID, &u.Name, &u.Email)

    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("用户 %d 不存在", id)
    }
    if err != nil {
        return nil, err
    }
    return &u, nil
}

插入 — db.Exec

func createUser(db *sql.DB, name, email string) (int64, error) {
    result, err := db.Exec(
        "INSERT INTO users (name, email) VALUES (?, ?)", name, email,
    )
    if err != nil {
        return 0, err
    }

    id, err := result.LastInsertId()
    if err != nil {
        return 0, err
    }
    return id, nil
}

更新与删除

func updateUser(db *sql.DB, id int, name, email string) error {
    result, err := db.Exec(
        "UPDATE users SET name = ?, email = ? WHERE id = ?", name, email, id,
    )
    if err != nil {
        return err
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        return fmt.Errorf("用户 %d 不存在", id)
    }
    return nil
}

func deleteUser(db *sql.DB, id int) error {
    result, err := db.Exec("DELETE FROM users WHERE id = ?", id)
    if err != nil {
        return err
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        return fmt.Errorf("用户 %d 不存在", id)
    }
    return nil
}

关键区别:db.Query 返回 *sql.Rows(需遍历和关闭);db.QueryRow 返回 *sql.Row(单行,自动关闭);db.Exec 返回 sql.Result(用于增删改)。

3 预处理语句

预处理语句(Prepared Statement)在需要重复执行相同 SQL 时能显著提升性能,同时天然防止 SQL 注入。

func batchInsertUsers(db *sql.DB, users []User) error {
    stmt, err := db.Prepare("INSERT INTO users (name, email) VALUES (?, ?)")
    if err != nil {
        return err
    }
    defer stmt.Close()

    for _, u := range users {
        _, err := stmt.Exec(u.Name, u.Email)
        if err != nil {
            return fmt.Errorf("插入用户 %s 失败: %w", u.Name, err)
        }
    }
    return nil
}

func searchUsers(db *sql.DB, keyword string) ([]User, error) {
    stmt, err := db.Prepare("SELECT id, name, email FROM users WHERE name LIKE ?")
    if err != nil {
        return nil, err
    }
    defer stmt.Close()

    rows, err := stmt.Query("%" + keyword + "%")
    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); err != nil {
            return nil, err
        }
        users = append(users, u)
    }
    return users, rows.Err()
}

SQL 注入防护:永远使用 ? 占位符传递参数,绝不要fmt.Sprintf 拼接 SQL 字符串。db.Querydb.Exec 的参数化查询也同样安全。

4 事务

事务确保一组数据库操作要么全部成功,要么全部回滚。Go 中使用 db.Begin() 开启事务。

基本事务流程

func transferMoney(db *sql.DB, fromID, toID int, amount float64) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    // defer 确保异常时回滚
    defer tx.Rollback()

    // 扣款
    _, err = tx.Exec(
        "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
        amount, fromID, amount,
    )
    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) VALUES (?, ?, ?)",
        fromID, toID, amount,
    )
    if err != nil {
        return fmt.Errorf("记录日志失败: %w", err)
    }

    // 全部成功,提交事务
    return tx.Commit()
}

封装通用事务函数

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 createUserWithProfile(db *sql.DB, name, email, bio string) error {
    return withTransaction(db, func(tx *sql.Tx) error {
        result, err := tx.Exec(
            "INSERT INTO users (name, email) VALUES (?, ?)", name, email,
        )
        if err != nil {
            return err
        }

        userID, _ := result.LastInsertId()
        _, err = tx.Exec(
            "INSERT INTO profiles (user_id, bio) VALUES (?, ?)", userID, bio,
        )
        return err
    })
}

defer + Rollback 模式:defer tx.Rollback() 后调用 tx.Commit()——如果 Commit 成功,后续的 Rollback 是空操作(不会报错);如果中途返回错误,defer 会自动回滚。

5 连接池与最佳实践

sql.DB 内部自带连接池,通过以下方法配置:

package main

import (
    "database/sql"
    "log"
    "time"

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

func initDB() *sql.DB {
    db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&parseTime=True")
    if err != nil {
        log.Fatal(err)
    }

    // 最大打开连接数(默认无限制)
    db.SetMaxOpenConns(25)

    // 最大空闲连接数(默认 2)
    db.SetMaxIdleConns(10)

    // 连接最大存活时间(建议小于数据库的 wait_timeout)
    db.SetConnMaxLifetime(5 * time.Minute)

    // 空闲连接最大存活时间
    db.SetConnMaxIdleTime(3 * time.Minute)

    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
    return db
}

func main() {
    db := initDB()
    defer db.Close()

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

💡 数据库最佳实践

sql.DB 全局唯一,通过依赖注入传递

始终检查 rows.Err()rows.Close()

使用 ? 占位符防 SQL 注入

配置合理的连接池参数

不要每次请求都 sql.Open

不要忘记 defer rows.Close()

不要用字符串拼接 SQL

不要在 for 循环中打开连接

6 本章要点

🔌 database/sql

  • sql.Open + 匿名导入驱动
  • db.Ping() 验证连接
  • • 统一接口,可切换数据库

📝 CRUD

  • db.Query 查询多行
  • db.QueryRow 查询单行
  • db.Exec 增删改操作

🔒 安全与事务

  • • 预处理语句防 SQL 注入
  • db.Begin / tx.Commit / tx.Rollback
  • • defer + Rollback 安全模式

⚙️ 连接池

  • SetMaxOpenConns 最大连接数
  • SetMaxIdleConns 空闲连接数
  • SetConnMaxLifetime 存活时间

🛡️ 最佳实践

  • • 全局单例 *sql.DB
  • • 始终 defer rows.Close()
  • • 参数化查询而非拼接

📦 常用驱动

  • • MySQL: go-sql-driver/mysql
  • • PostgreSQL: lib/pq, pgx
  • • SQLite: mattn/go-sqlite3