第 8 章 · database/sql 与 MySQL 驱动
本章目标:掌握 Go 标准库 database/sql 与 go-sql-driver/mysql 访问 shop_db;使用 PreparedStatement 风格参数化查询完成 CRUD;理解 defer rows.Close() 与连接生命周期;封装 ProductRepository;贯彻 slug + is_published + price(分) 读写约定。
学时建议:5~6 小时(含 2.5 小时跟练)
前置:完成 go-database ch07(shop_db DDL 已就绪);go-dev ch01~ch08。
8.1 场景说明:不用 ORM 也要会访问数据库
在 shop-db 运维脚本、数据迁移、单元测试夹具中,常需轻量直连 MySQL,而不引入 GORM。本章用标准库完成:
| 任务 | 包/文件 | 技术 |
|---|---|---|
| 初始化库 | cmd/seed/main.go | 执行 ch07 DDL |
| 插入分类/商品 | internal/repo/product_insert.go | INSERT + ? 占位 |
| 查询上架商品 | internal/repo/product_list.go | SELECT + Scan |
| 更新库存 | internal/repo/stock_update.go | UPDATE + 事务 |
| 删除草稿 | internal/repo/product_delete.go | DELETE + 条件 |
路径:~/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=true | time.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()
}
字段契约:price 用 int64 分;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)