Go 操作 MySQL:常用写法与最佳实践

Go 操作 MySQL:常用写法与最佳实践

2026年6月,Go + MySQL 依然是后端开发的黄金组合。 但能连上和写好,是两回事。


一、先搞清一个核心:database/sql + 驱动,不是 ORM

Go 官方不提供 MySQL 原生驱动。标准做法是:

组件 作用 包路径
database/sql 通用数据库接口抽象层 标准库
go-sql-driver/mysql MySQL 驱动实现 github.com/go-sql-driver/mysql

记住:必须空白导入驱动,否则报 sql: unknown driver "mysql"。

go 复制代码
go
1import (
2    "database/sql"
3    _ "github.com/go-sql-driver/mysql"  // ← 这个下划线不能省
4)
5

二、连接:sql.Open 不等于真的连上了

这是新手最大的坑。sql.Open 只初始化连接池,不建立连接 。真正连通靠 Ping。

go 复制代码
go
1dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
2db, err := sql.Open("mysql", dsn)
3if err != nil {
4    log.Fatal(err)
5}
6defer db.Close()
7
8// 必须显式 Ping,否则第一次查询才会真正连接
9if err := db.Ping(); err != nil {
10    log.Fatal(err)
11}
12

DSN 参数说明:

参数 作用 不加会怎样
parseTime=True DATETIME 自动转 time.Time 查出来是 []byte
loc=Local 按时区解析时间 时间差8小时
charset=utf8mb4 字符集 中文乱码

三、连接池:不配置等于裸奔

默认 MaxOpenConns=0(无上限)、MaxIdleConns=2。并发一上来,MySQL 直接被打满。

推荐配置(Web 应用) :

scss 复制代码
go
1db.SetMaxOpenConns(25)      // 最大并发连接数
2db.SetMaxIdleConns(25)      // 最大空闲连接数(建议与 MaxOpenConns 相同)
3db.SetConnMaxLifetime(5 * time.Minute)  // 连接最长存活时间
4db.SetConnMaxIdleTime(30 * time.Minute) // 空闲连接回收时间
5
参数 推荐值 为什么
MaxOpenConns 10~50 超过 MySQL 承载能力会雪崩
MaxIdleConns 5~10 太少频繁建连,太多浪费资源
ConnMaxLifetime 30分钟 必须小于 MySQL 的 wait_timeout(云数据库常设300秒)

实测数据:max=20, minIdle=5 时 QPS 达 1420,延迟 63ms;max=10, minIdle=2 时 QPS 仅 850,延迟 118ms。


四、CRUD:四种写法,一次讲透

4.1 插入(INSERT)

go 复制代码
go
1// 单条插入
2res, err := db.Exec("INSERT INTO users(name, age) VALUES (?, ?)", "Alice", 25)
3if err != nil {
4    log.Fatal(err)
5}
6id, _ := res.LastInsertId()  // 自增ID
7
8// 批量插入(比循环 Exec 快一个数量级)
9res, err := db.Exec(
10    "INSERT INTO users(name, age) VALUES (?, ?), (?, ?), (?, ?)",
11    "Bob", 30, "Carol", 28, "Dave", 35,
12)
13

4.2 查询单行(QueryRow)

go 复制代码
go
1var name string
2var age int
3err := db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)
4if err != nil {
5    if err == sql.ErrNoRows {
6        // 记录不存在,这是唯一可预期的"无结果"错误
7        return nil, fmt.Errorf("user not found")
8    }
9    log.Fatal(err)
10}
11

关键点:QueryRow 返回的 *sql.Row 必须调用 Scan,否则游标不释放,连接泄漏。

4.3 查询多行(Query)

go 复制代码
go
1rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ?", 18)
2if err != nil {
3    log.Fatal(err)
4}
5defer rows.Close()  // ← 不 defer 就泄漏
6
7var users []User
8for rows.Next() {
9    var u User
10    if err := rows.Scan(&u.ID, &u.Name, &u.Age); err != nil {
11        log.Fatal(err)
12    }
13    users = append(users, u)
14}
15if err := rows.Err(); err != nil {  // 检查迭代过程中的错误
16    log.Fatal(err)
17}
18

4.4 更新(UPDATE)

go 复制代码
go
1res, err := db.Exec("UPDATE users SET age = ? WHERE name = ?", 30, "Alice")
2if err != nil {
3    log.Fatal(err)
4}
5affected, _ := res.RowsAffected()  // 受影响行数
6

4.5 删除(DELETE)

go 复制代码
go
1res, err := db.Exec("DELETE FROM users WHERE id = ?", 123)
2if err != nil {
3    log.Fatal(err)
4}
5affected, _ := res.RowsAffected()
6

五、事务:Go 不会自动回滚

这是最容易翻车的地方。一旦 tx, err := db.Begin() 成功,必须显式 Commit 或 Rollback,否则连接一直被占用直到超时。

go 复制代码
go
1tx, err := db.Begin()
2if err != nil {
3    return err
4}
5
6// 关键:用 defer 确保 Rollback 只执行一次
7defer func() {
8    if p := recover(); p != nil {
9        tx.Rollback()
10        panic(p)
11    }
12}()
13
14_, err = tx.Exec("INSERT INTO logs(msg) VALUES (?)", "start")
15if err != nil {
16    tx.Rollback()
17    return err
18}
19
20_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = ?", 1)
21if err != nil {
22    tx.Rollback()
23    return err
24}
25
26// 全部成功才提交
27if err := tx.Commit(); err != nil {
28    return err
29}
30

三条铁律:

  1. 事务内所有 SQL 必须用 tx.Query / tx.Exec,绝不能用 db.Query
  2. defer tx.Rollback() 写在最前面,成功路径末尾 tx.Commit()
  3. 用 context 控制超时:db.BeginTx(ctx, nil)

六、预编译语句:高频场景的性能利器

go 复制代码
go
1stmt, err := db.Prepare("INSERT INTO users(name, age) VALUES (?, ?)")
2if err != nil {
3    log.Fatal(err)
4}
5defer stmt.Close()
6
7// 复用 stmt,避免重复解析 SQL
8_, err = stmt.Exec("John", 25)
9_, err = stmt.Exec("Jane", 28)
10

好处: SQL 只解析一次,防止 SQL 注入,批量执行时性能提升明显。


七、Scan 踩坑清单

坑 现象 解决方案
字段数不匹配 sql: expected 3 destination arguments in Scan 用具体字段名,不用 SELECT *
NULL 值 Scan panic 或结果为零值 用 sql.NullString / sql.NullInt64 接收
TINYINT(1) 当 bool Go 里读出来是 0/1 不是 true/false 明确用 int8 接收
时区错乱 时间差8小时 DSN 加 loc=Local

八、Web 应用中的依赖注入

别用全局变量,别搞自定义 Context 封装。函数式依赖注入才是正解:

go 复制代码
go
1// main.go
2func main() {
3    db, _ := initDB()  // 全局唯一实例
4    http.HandleFunc("/users", handlers.UsersHandler(db))
5    http.ListenAndServe(":8000", nil)
6}
7
8// handlers/users.go
9func UsersHandler(db *sql.DB) http.HandlerFunc {
10    return func(w http.ResponseWriter, r *http.Request) {
11        rows, err := db.Query("SELECT id, name FROM users")
12        if err != nil {
13            http.Error(w, "DB error", 500)
14            return
15        }
16        defer rows.Close()
17        // ...
18    }
19}
20

好处 : 依赖清晰可见,测试时用 sqlmock 轻松替换,避免 init() 顺序陷阱。


九、一张图总结最优架构

sql 复制代码
1HTTP 请求
2    │
3    ▼
4Handler(函数式注入 *sql.DB)
5    │
6    ├── Query / Exec(普通操作)
7    ├── db.Prepare(预编译,高频复用)
8    └── db.BeginTx(事务,必须 Commit/Rollback)
9    │
10    ▼
11连接池(MaxOpenConns=25, MaxIdleConns=25, Lifetime=5min)
12    │
13    ▼
14go-sql-driver/mysql → MySQL
15

十、2026年的新变量

MySQL 9.6.0(2026 Linux 创新版)已将外键约束上移至 SQL 层,所有变更完整记录至 Binlog,CDC 和主从复制的数据一致性问题基本解决。如果你在用云数据库,SetConnMaxLifetime 务必小于云厂商的连接超时(通常 300 秒),否则复用连接时会遭遇 invalid connection。


写在最后

Go 操作 MySQL 的核心就一句话:连接池管好,事务别忘回滚,Scan 字段对齐。

能做到这三点,你已经超过 80% 的 Go 开发者了。剩下 20% 的差距,在错误处理和可测试性上------而那恰恰是工程能力的分水岭。

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码5 天前
别再前后端各写一套表单校验了
java·后端
大勇前进5 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu5 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile5 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白805 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙5 天前
PHP 接口返回统一响应封装,让前后端对接更省心
后端
盖伦发发5 天前
软件工程SOLID 五大设计原则
后端·软件工程