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() 成功,必须显式 CommitRollback,否则连接一直被占用直到超时。

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% 的差距,在错误处理和可测试性上------而那恰恰是工程能力的分水岭。

相关推荐
小闹5491 天前
Claude Code 给自己接了一部飞书,从此不用守在工位等它
后端·claude
浮游本尊1 天前
Java学习第41天 - 复杂查询、多表关联、索引优化与慢 SQL 调优
后端
llz_1121 天前
web-第五次课后作业
前端·后端·http
雨辰AI1 天前
生产级实测:SpringBoot3 + 达梦数据库接口从 200ms 优化至 20ms 完整调优指南
java·数据库·spring boot·后端·政务
Solis1 天前
Raft:分布式系统的定海神针
后端·架构
程序员老申1 天前
第三篇 5 天 12 个 commit:踩坑实录与代码演进
后端·程序员
程序员鱼皮1 天前
提示词工程已死,Loop Engineering 称王!保姆级教程 + 项目实战
前端·后端·ai编程
Mininglamp_27181 天前
Vibe Coding 之后是 Vibe Operating?
后端·开源·多智能体·ai agent·mano-p
星哥的编程之路1 天前
别再调 API 就说自己会 RAG 了,看看真正的企业级 AI 智能体长什么样
后端·面试
长大19881 天前
C++26 静态反射完整实战:告别宏代码生成,一键实现序列化
后端