用 database/sql 配合 github.com/go-sql-driver/mysql 是最稳妥的 mysql 连接方案,需空白导入驱动、正确配置 dsn、合理设置连接池参数,并严格区分 db 与 tx 的使用场景。

用 database/sql 配合 mysql 驱动是最稳妥的选择
Go 官方不提供 MySQL 原生驱动,database/sql 是标准接口,必须搭配第三方驱动(如 github.com/go-sql-driver/mysql)才能连 MySQL。别试图自己封装 TCP 连接或调用 C 库——既不安全也不可持续。
安装驱动:
go get -u github.com/go-sql-driver/mysql注意:不是
go install,这个包不含可执行文件,go get 就够了。常见错误现象:sql: unknown driver "mysql" (forgotten import?)——说明你写了 sql.Open("mysql", ...) 却没导入驱动包。驱动注册靠的是 _ "github.com/go-sql-driver/mysql" 这种空白导入,少这行就报错。
- 导入时必须带下划线前缀:
import _ "github.com/go-sql-driver/mysql" - DNS 字符串格式要严格:
user:password@tcp(127.0.0.1:3306)/dbname?parseTime=true&loc=Local,&要写成&(XML/HTML 实体),否则解析失败 -
parseTime=true才能让time.Time正确扫描DATETIME字段;不加的话查出来是字符串或 nil
sql.DB 不是单个连接,别把它当“数据库句柄”反复创建
sql.DB 是连接池抽象,不是连接本身。它线程安全,应作为全局变量长期持有,而不是每次请求都 sql.Open() 一次——后者会快速耗尽文件描述符,触发 dial tcp: lookup xxx: no such host 或 too many open files。
初始化示例:
var db *sql.DB<br><br>func initDB() error {<br> dsn := "user:pass@tcp(localhost:3306)/test?parseTime=true&loc=Local"<br> var err error<br> db, err = sql.Open("mysql", dsn)<br> if err != nil {<br> return err<br> }<br> // 设置连接池参数,防止默认值引发雪崩<br> db.SetMaxOpenConns(25)<br> db.SetMaxIdleConns(25)<br> db.SetConnMaxLifetime(5 * time.Minute)<br> return db.Ping()<br>}
-
sql.Open()不校验连接,只做配置解析;真正连通靠db.Ping() -
SetMaxOpenConns和SetMaxIdleConns建议设为相同值,避免空闲连接被过早回收又频繁新建 - MySQL 默认 wait_timeout 是 8 小时,但云数据库(如阿里云 RDS)常设为 300 秒,
SetConnMaxLifetime必须小于它,否则连接复用时可能遇到invalid connection
用 QueryRow() / Exec() 时,记得检查 err,别只看返回值
Go 的惯用法是「显式错误处理」,但很多人只关注 Scan() 或返回的 Result,忽略前置操作本身的 error。比如 db.QueryRow("SELECT id FROM users WHERE id = ?", 123).Scan(&id),如果表不存在或字段名错,QueryRow() 返回的 *sql.Row 已含错误,Scan() 会直接返回那个错——但你可能误以为是 Scan 出问题。
正确姿势是分两步判断:
row := db.QueryRow("SELECT name FROM users WHERE id = ?", userID)<br>var name string<br>err := row.Scan(&name)<br>if err != nil {<br> if errors.Is(err, sql.ErrNoRows) {<br> // 处理未找到<br> return nil, fmt.Errorf("user not found")<br> }<br> return nil, err<br>}
-
sql.ErrNoRows是唯一可预期的「无结果」错误,其他err都该按异常处理 -
Exec()返回sql.Result,但它的LastInsertId()在 MySQL 中仅对INSERT有效,且需开启CLIENT_FOUND_ROWS(驱动默认不开),否则可能返回 0 - 批量插入别硬拼 SQL,用
VALUES (?, ?), (?, ?), ...配合Exec(),比循环调用Exec()快一个数量级
事务里别混用 db.Query() 和 tx.Query()
一旦开始事务,所有读写操作必须走 *sql.Tx 对象,不能穿插使用原始 *sql.DB。否则看似成功,实际数据没进事务上下文,tx.Commit() 后发现某些操作早已提交,破坏一致性。
典型错误:
tx, _ := db.Begin()<br>tx.Exec("INSERT INTO logs (...) VALUES (...)") // ✅<br>rows, _ := db.Query("SELECT * FROM config") // ❌ 错!该用 tx.Query
- 事务中所有 SQL 必须用
tx.Query、tx.QueryRow、tx.Exec - 事务超时控制靠应用层:设置
context.WithTimeout,传给db.BeginTx(ctx, nil),否则挂起的事务可能锁表数小时 - MySQL 的
READ COMMITTED是默认隔离级别,但 Go 驱动不自动设,需显式传&sql.TxOptions{Isolation: sql.LevelReadCommitted}
事务的边界和错误传播最容易被忽略:一个 tx.QueryRow().Scan() 报错后,若没 tx.Rollback() 就直接 return,这个 tx 会一直 dangling,直到连接超时被服务端 kill——期间它占着锁,影响其他请求。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











