Build Web Application with Golang 数据库实战:基于 database/sql 的 MySQL 驱动选型与 CRUD 编程
Build Web Application with Golang 数据库实战:基于 database/sql 的 MySQL 驱动选型与 CRUD 编程
本文是《Build Web Application with Golang》第五章数据库篇的核心实战章节,围绕 Go 语言生态中最流行的 MySQL 访问方式展开:先对比两款纯 Go 实现的 MySQL 驱动,再以 database/sql 标准接口为骨架,给出完整的建表 SQL、增删改查示例与 DSN 连接串格式,并佐以本仓库 ch.5.2 配套源码 的运行步骤。读完本文,你将掌握在 Go Web 后端中安全、稳定地接入 MySQL 的完整套路,包括驱动注册原理、连接串编写、预处理语句防注入以及结果集遍历。
MySQL 在 LAMP 中的地位与 Go 的接入方式
近年来 LAMP 架构在互联网领域非常流行,其中 M 代表 MySQL。MySQL 之所以成为众多网站后端的默认数据库,是因为它开源、易用,且与 Linux、Apache、PHP 的组合生态成熟。在《Build Web Application with Golang》的数据库章节(见 第五章导览)中,作者将 SQL 数据库驱动(MySQL、SQLite、PostgreSQL)与 NoSQL(MongoDB、Redis)并列介绍,而 MySQL 正是第一个被详细讲解的 SQL 数据库。
与 PHP 等语言自带官方数据库扩展不同,Go 语言本身不提供任何官方数据库驱动,而是通过标准库 database/sql 定义了一套驱动接口规范,第三方开发者可以基于该规范实现各自的驱动。这套接口的完整设计(sql.Register、driver.Driver、driver.Conn、driver.Stmt、driver.Tx、driver.Result 等)在 5.1 节 database/sql 接口 中有详细说明。理解这套接口是本节 MySQL 实战的前提:
sql.Register(name, driver):第三方驱动在自己的init()中调用,将驱动注册进database/sql内部的map[string]driver.Driver;- 导入时使用
_ "github.com/go-sql-driver/mysql"这种空白导入,目的就是只执行驱动的init()注册逻辑而不直接引用其包符号; database/sql在驱动之上维护了一个简单的连接池(DB结构中的freeConn []driver.Conn),复用空闲连接、按需新建连接。
因此,下面所有示例都遵循"注册驱动 → 打开连接 → 预处理 → 执行/查询 → 关闭"的统一流程,这套流程对任何实现了 database/sql 接口的驱动都适用。
Go 的 MySQL 驱动选型
Go 生态中支持 MySQL 的驱动不止一个,它们在标准接口支持上有所差异。本书 5.2 节列出两款纯 Go 编写的驱动:
| 驱动 | 接口支持 | 实现语言 | 特点 |
|---|---|---|---|
| go-sql-driver/mysql | 完整支持 database/sql |
纯 Go | 较新的驱动,功能更丰富,支持 keep-alive 长连接且线程安全 |
| ziutek/mymysql | 支持 database/sql 及自定义接口 |
纯 Go | 另有一套用户自定义的接口标准 |
作者在书中明确表示,个人项目使用并推荐 go-sql-driver/mysql,理由有三点:
- 它是一款较新的数据库驱动,支持更多特性;
- 它完全支持
database/sql接口标准; - 支持 keep-alive 与线程安全的长连接。
这意味着在后续开发 ORM 或业务代码时,只要坚持使用 database/sql 标准接口,未来即使更换数据库驱动,业务代码也无需改动——这正是 5.1 节 反复强调的接口解耦价值。
建表与驱动依赖准备
统一的数据表结构
本书 5.2~5.4 节(MySQL、SQLite、PostgreSQL)共用同一张业务表,便于读者横向对比三种数据库的驱动差异。建表 SQL 如下:
CREATE TABLE `userinfo` (
`uid` INT(10) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(64) NULL DEFAULT NULL,
`department` VARCHAR(64) NULL DEFAULT NULL,
`created` DATE NULL DEFAULT NULL,
PRIMARY KEY (`uid`)
);
仓库中对应的可执行脚本位于 en/code/src/apps/ch.5.2/schema.sql。注意其中的字段名是 department,而配套 main.go 中 INSERT 语句使用的列名是 departname,二者略有出入,运行前请以实际表结构为准调整 SQL 列名。
拉取驱动依赖
示例代码通过空白导入引用驱动包:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
根据 ch.5.2 readme 的说明,需要先执行 go get 下载并安装远程驱动包,再运行程序:
go get github.com/go-sql-driver/mysql
go run main.go
完整 CRUD 示例代码
以下是基于 database/sql 接口标准对 MySQL 进行增、删、改、查的完整示例(与书中代码同源,仓库版本见 en/code/src/apps/ch.5.2/main.go):
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
checkErr(err)
// insert
stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?")
checkErr(err)
res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
checkErr(err)
id, err := res.LastInsertId()
checkErr(err)
fmt.Println(id)
// update
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
checkErr(err)
res, err = stmt.Exec("astaxieupdate", id)
checkErr(err)
affect, err := res.RowsAffected()
checkErr(err)
fmt.Println(affect)
// query
rows, err := db.Query("SELECT * FROM userinfo")
checkErr(err)
for rows.Next() {
var uid int
var username string
var department string
var created string
err = rows.Scan(&uid, &username, &department, &created)
checkErr(err)
fmt.Println(uid)
fmt.Println(username)
fmt.Println(department)
fmt.Println(created)
}
rows.Close()
// delete
stmt, err = db.Prepare("delete from userinfo where uid=?")
checkErr(err)
res, err = stmt.Exec(id)
checkErr(err)
affect, err = res.RowsAffected()
checkErr(err)
fmt.Println(affect)
db.Close()
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
仓库中的可运行版本进一步做了两点工程化改进,更接近真实项目写法:
- 用常量集中管理连接信息,DSN 通过
fmt.Sprintf动态拼接:
const (
DB_USER = "user"
DB_PASSWORD = ""
DB_NAME = "test"
)
dbSouce := fmt.Sprintf("%v:%v@/%v?charset=utf8", DB_USER, DB_PASSWORD, DB_NAME)
db, err := sql.Open("mysql", dbSouce)
- 用
defer db.Close()确保连接最终释放,而不是在函数末尾手动关闭;查询结果的打印也使用了fmt.Printf按列对齐输出。
逐段拆解:连接、预处理、执行与遍历
本节代码中每个关键函数的职责如下:
sql.Open():打开一个已注册的数据库驱动。第一个参数"mysql"正是 go-sql-driver/mysql 在init()中通过sql.Register("mysql", ...)注册的名字;第二个参数是 DSN(Data Source Name,数据源名称),定义数据库连接所需信息,支持以下格式:
user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
第一行表示通过 Unix 套接字连接;第二行是通过 TCP 连接并显式指定端口;第三行是省略 host/port 的本地默认连接;第四行展示了 IPv6 地址的写法(地址需用方括号包裹)。?charset=utf8 是常见参数,用于指定连接字符集。
-
db.Prepare():返回一条待执行的 SQL 语句对象(Stmt),同时返回 SQL 执行状态。预处理语句把 SQL 结构与参数分离,SQL 骨架只解析一次,参数在Exec时再绑定。 -
db.Query():执行 SQL 查询并返回Rows结果集。 -
stmt.Exec():执行已经通过Prepare准备好的语句,用于 insert/update/delete 等写操作;返回的Result可通过LastInsertId()拿到自增主键,通过RowsAffected()拿到受影响行数。 -
rows.Next()+rows.Scan():结果集遍历的标准姿势——Next()判断是否还有下一行,Scan()按目标变量的类型顺序把当前行各列值填充进对应指针。遍历结束后应调用rows.Close()释放迭代器资源。
从 5.1 节 的接口定义可以进一步理解底层调用链:db.Prepare 内部通过 driver.Conn.Prepare 得到 driver.Stmt,stmt.Exec 对应 driver.Stmt.Exec(args []Value),rows.Next 对应 driver.Rows.Next(dest []Value);而 database/sql 层在其之上自动完成了连接池的借还与复用。
关于 SQL 注入防护
示例中所有传参均使用 =? 占位符格式,例如:
stmt.Exec("astaxie", "研发部门", "2012-12-09")
stmt.Exec("astaxieupdate", id)
这是防止 SQL 注入攻击的必要措施:参数以独立数据形式交给驱动绑定,而不是直接拼接进 SQL 字符串,从而避免恶意输入改变语句结构。在 Web 表单提交、URL 参数等不可信输入进入 SQL 之前,务必统一走预处理占位符,不要使用字符串拼接构造 SQL。
运行与验证步骤
参照 ch.5.2 readme,完整运行本示例的步骤为:
- 安装并启动 MySQL,确保本地服务可连接;
- 创建用户与数据库,与
main.go顶部的常量保持一致:
DB_USER = "user"
DB_PASSWORD = ""
DB_NAME = "test"
- 创建
userinfo表,执行 schema.sql 中的建表语句; go get下载安装远程驱动包github.com/go-sql-driver/mysql;go run main.go执行程序,观察控制台依次输出插入行的自增 ID、更新与删除的受影响行数,以及查询结果的逐行打印。
仓库源码 en/code/src/apps/ch.5.2/main.go 的输出带有更清晰的阶段标记(Inserting / Updating / Querying / Deleting),便于对照每一步的执行顺序。
小结与延伸阅读
本节围绕 database/sql 标准接口,完成了从驱动选型、DSN 编写、建表到增删改查的 MySQL 接入闭环,并强调了 =? 占位符对 SQL 注入的防御作用。这套代码模式将在后续章节继续复用:5.3 节改用 SQLite 驱动、5.4 节改用 PostgreSQL 驱动时,业务代码结构几乎不变,这正是 Go 数据库接口设计带来的迁移成本优势。
延伸阅读:
- 数据库章节导览:了解 SQL 与 NoSQL 的章节组织;
- database/sql 接口设计:驱动注册、连接池与底层接口原理;
- SQLite 实战 与 PostgreSQL 实战:对比不同 SQL 驱动的接入方式;
- 5.5 节 ORM 封装:基于
database/sql标准接口封装的 ORM 使用方式。