下载工作台
Go 数据库实战

database/sql 与 MySQL 驱动

试读上半部分 · 解锁后可读全文

第 8 章 · database/sql 与 MySQL 驱动

本章目标:掌握 Go 标准库 database/sqlgo-sql-driver/mysql 访问 shop_db;使用 PreparedStatement 风格参数化查询完成 CRUD;理解 defer rows.Close() 与连接生命周期;封装 ProductRepository;贯彻 slug + is_published + price(分) 读写约定。

学时建议:5~6 小时(含 2.5 小时跟练)

前置:完成 go-database ch07shop_db DDL 已就绪);go-dev ch01~ch08


8.1 场景说明:不用 ORM 也要会访问数据库

shop-db 运维脚本、数据迁移、单元测试夹具中,常需轻量直连 MySQL,而不引入 GORM。本章用标准库完成:

任务包/文件技术
初始化库cmd/seed/main.go执行 ch07 DDL
插入分类/商品internal/repo/product_insert.goINSERT + ? 占位
查询上架商品internal/repo/product_list.goSELECT + Scan
更新库存internal/repo/stock_update.goUPDATE + 事务
删除草稿internal/repo/product_delete.goDELETE + 条件
路径:~/go-learn/shop-db;数据库:shop_db

8.2 go mod 依赖

// go.mod
module github.com/example/shop-db

go 1.22

require github.com/go-sql-driver/mysql v1.8.1

安装:

go get github.com/go-sql-driver/mysql@v1.8.1

DSN 格式(勿硬编码生产密码):

user:pass@tcp(127.0.0.1:3306)/shop_db?parseTime=true&loc=Local&charset=utf8mb4
参数说明
parseTime=truetime.Time 扫描 DATETIME
charset=utf8mb4完整 Unicode
loc=Local本地时区

8.3 打开连接与 Ping

internal/database/mysql.go

package database

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

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

func OpenMySQL() (*sql.DB, error) {
    dsn := os.Getenv("DB_DSN")
    if dsn == "" {
        dsn = "shop_user:shop_pass@tcp(127.0.0.1:3306)/shop_db?parseTime=true&loc=Local&charset=utf8mb4"
    }
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, fmt.Errorf("open mysql: %w", err)
    }
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(30 * time.Minute)
    if err := db.Ping(); err != nil {
        _ = db.Close()
        return nil, fmt.Errorf("ping mysql: %w", err)
    }
    return db, nil
}
sql.Open 不会立即连库,首次 Ping/Query 才建连。

8.4 查询与 Scan

type Product struct {
    ID           int64
    Slug         string
    Title        string
    PriceCents   int64  // 分
    IsPublished  bool
    CategoryID   int64
}

func ListPublished(ctx context.Context, db *sql.DB) ([]Product, error) {
    const q = `
        SELECT id, slug, title, price, is_published, category_id
        FROM products
        WHERE is_published = 1
        ORDER BY id DESC
        LIMIT 100`
    rows, err := db.QueryContext(ctx, q)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var out []Product
    for rows.Next() {
        var p Product
        var published int
        if err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.PriceCents, &published, &p.CategoryID); err != nil {
            return nil, err
        }
        p.IsPublished = published == 1
        out = append(out, p)
    }
    return out, rows.Err()
}

字段契约priceint64 分is_published MySQL 用 TINYINT(1),Scan 到 int 再转 bool。


8.5 参数化 INSERT / UPDATE

func InsertProduct(ctx context.Context, db *sql.DB, p Product) (int64, error) {
    const q = `
        INSERT INTO products (slug, title, price, is_published, category_id, stock)
        VALUES (?, ?, ?, ?, ?, ?)`
    res, err := db.ExecContext(ctx, q,
        p.Slug, p.Title, p.PriceCents, boolToTiny(p.IsPublished), p.CategoryID, 100,
    )
    if err != nil {
        return 0, err
    }
    return res.LastInsertId()
}

func boolToTiny(b bool) int {
    if b {
        return 1
    }
    return 0
}

禁止字符串拼接 SQL,防注入:

// 错误示范 — 切勿在生产使用
// q := fmt.Sprintf("SELECT * FROM products WHERE slug='%s'", slug)

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。