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

相关推荐
马良神笔11 小时前
2026 年Rag最近技术前沿总结
后端·python·django
程序猿DD11 小时前
同样10美元,OpenDesign和Command Code的DeepSeek V4.1 Flash额度到底有多少?
后端
高频因子挖掘机11 小时前
A股量化从日线升级到5分钟线:数据量增加后最先暴露哪些工程问题?
后端·github·api
高频因子挖掘机12 小时前
历史行情返回顺序变了,为什么按行号计算指标很危险?
后端·github·api
团子哒哒13 小时前
数组、切片和Map
后端
专注仿真13 小时前
ThinkPHP6 API接口开发实例:从零搭建RESTful风格接口
后端·php·restful
开心猴爷14 小时前
应对iOS审核4.3:可以使用 IpaGuard 来给属性名混淆的技术细节与平衡之道
后端·ios
1m5555555514 小时前
RAG 工程实战(五):正确知识已经进入 Prompt,为什么大模型还是会答错?
后端·面试
易小尊14 小时前
我的模型在 65% 以上时准得可怕,在 50% 时像在掷硬币
后端
用户27886072856714 小时前
fallback 会盖掉平台提示词:system、空数组和 systemAppend 的差别
前端·后端