- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
本文是《build-web-application-with-golang》第 5.2 节的深入解读,面向需要在 Go Web 后端中接入 MySQL 的开发者。全文以database/sql标准接口为骨架,覆盖 MySQL 驱动的选型与理由、DSN 连接串的完整格式、预编译语句(Prepare/Exec)的增删改查(CRUD)实战,并结合仓库内的配套示例代码(main.go、schema.sql)讲解底层执行原理与防 SQL 注入的机制。读完本文,你将能够独立完成 Go 程序与 MySQL 的连接、建表、插入、更新、查询与删除,并理解database/sql连接池与驱动注册的工作方式。
MySQL 在 Go Web 架构中的位置
目前 Internet 上流行的网站构架方式是 LAMP,其中的 M 即 MySQL。作为数据库,MySQL 以免费、开源、使用方便等优势,成为了很多 Web 开发的后端数据库存储引擎。在 Go 生态中,Go 官方并没有提供数据库驱动,而是通过database/sql标准库定义了一组数据库驱动接口,开发者只需按照这套标准接口编写驱动,上层业务代码就可以在将来迁移数据库时做到最小改动。上一节 database/sql 接口 已经详细介绍了sql.Register、driver.Driver、driver.Conn、driver.Stmt、driver.Tx等底层接口,本节则以 MySQL 为例,展示在这套标准接口之上完成真实 CRUD 的全部步骤。
Go 中可用的 MySQL 驱动
Go 中支持 MySQL 的驱动目前比较多,大致可以分为两类:一类支持database/sql标准接口,另一类采用了自己的实现接口。常用的有如下几种:
- go-sql-driver/mysql:支持
database/sql,全部采用 Go 编写。当前仓库各语言版本配套代码(如 en/code/src/apps/ch.5.2/main.go)均使用该驱动。 - ziutek/mymysql:支持
database/sql,也支持自定义的接口,全部采用 Go 编写。 - Philio/GoMySQL:不支持
database/sql,使用自定义接口,全部采用 Go 编写。
本书接下来的例子主要以第一个驱动为例(这也是作者实际项目中采用的驱动),并推荐读者使用它,主要理由如下:
- 这个驱动比较新,维护得比较好;
- 完全支持
database/sql接口,业务代码不绑定具体驱动,便于将来迁移; - 支持 keepalive,保持长连接。虽然 mikespook fork 的 mymysql 也支持 keepalive,但不是线程安全的,而 go-sql-driver/mysql 从底层就支持了 keepalive。
需要特别说明的是:go-sql-driver/mysql 属于第三方驱动,需要单独安装。仓库配套示例的 readme(en/code/src/apps/ch.5.2/readme.md)给出了标准安装步骤,其中第 4 步就是执行go get下载并安装远程依赖包。这也是main.go中通过_ "github.com/go-sql-driver/mysql"匿名导入该驱动的前提。
示例数据库表结构
接下来的几个小节(包括 使用 SQLite、PostgreSQL 等)都将采用同一个数据库结构:数据库test,用户表userinfo,关联用户信息表userdetail。建表 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`) ); CREATE TABLE `userdetail` ( `uid` INT(10) NOT NULL DEFAULT '0', `intro` TEXT NULL, `profile` TEXT NULL, PRIMARY KEY (`uid`) )仓库中的 schema.sql 与上述表结构一致:uid为自增主键(AUTO_INCREMENT),username、department、created分别记录用户名、所属部门与创建日期。userdetail通过uid与userinfo建立一对一关联,用于扩展用户的个人介绍(intro)与画像(profile)信息。
在运行示例前,请先确保本机 MySQL 已安装并启动,且已经创建好test数据库与访问用户。配套 readme(en/code/src/apps/ch.5.2/readme.md)建议的步骤如下:
- 安装并运行 MySQL;
- 按照
main.go中的常量创建用户与数据库:
const ( DB_USER = "user" DB_PASSWORD = "" DB_NAME = "test" )- 执行 schema.sql 创建
userinfo表; - 执行
go get下载并安装远程依赖包; - 用
go run main.go运行程序。
使用 database/sql 接口对 MySQL 执行增删改查
下面这个示例演示了如何使用database/sql接口对数据库表进行增删改查操作。这是原文档的核心代码,也是仓库 en/code/src/apps/ch.5.2/main.go 的实现基础:
package main import ( "database/sql" "fmt" //"time" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") checkErr(err) //插入数据 stmt, err := db.Prepare("INSERT INTO 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) //更新数据 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) //查询数据 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) } //删除数据 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) } }仓库中的 main.go 在细节上做了一些工程化调整:它把数据库账号、密码、库名抽成了DB_USER、DB_PASSWORD、DB_NAME三个常量,用fmt.Sprintf动态拼接 DSN,并在每个操作前打印Inserting、Updating、Querying、Deleting提示,还用defer db.Close()替代了手动关闭。这些做法在真实项目中更易维护,但其 CRUD 调用链与原文档完全一致。
通过上面的代码我们可以看出,Go 操作 MySQL 数据库是很方便的:整个过程只依赖database/sql标准接口,与具体数据库驱动解耦,驱动只需在init函数中通过sql.Register完成注册(详见 database/sql 接口),上层代码即可直接使用。
关键的几个函数解释
sql.Open():用来打开一个注册过的数据库驱动。go-sql-driver/mysql 在包初始化时注册了mysql这个驱动名称,所以第一个参数传入"mysql";第二个参数是 DSN(Data Source Name),由 go-sql-driver 定义的数据库连接与配置信息组成。DSN 支持如下格式:
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各格式含义拆解如下:
| DSN 格式 | 连接方式 | 说明 |
|---|---|---|
user@unix(/path/to/socket)/dbname?charset=utf8 | Unix 域套接字 | 通过本地 socket 文件连接,常用于本机 MySQL |
user:password@tcp(localhost:5555)/dbname?charset=utf8 | TCP | 指定主机与端口,默认端口为 3306 |
user:password@/dbname | 默认协议 | 不显式写协议时使用驱动默认的本地连接方式 |
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname | IPv6 TCP | 主机为 IPv6 地址时需要加方括号 |
参数?charset=utf8用于指定连接使用的字符集,避免中文乱码。需要说明的是,sql.Open只是校验参数并返回*sql.DB对象,真正的网络连接会在第一次实际使用时才建立。
db.Prepare():返回准备要执行的 SQL 操作,然后返回准备完毕的执行状态(*sql.Stmt)。它对应 database/sql 接口 中driver.Stmt的Prepare概念——SQL 会被预编译,后续可以反复用不同参数执行,减少重复解析开销。
db.Query():直接执行 SQL 并返回*sql.Rows结果集,适用于 SELECT 查询。Rows对应 database/sql 接口 中的driver.Rows,通过rows.Next()逐行迭代,再调用rows.Scan()把当前行的列值按顺序写入传入的指针变量。
stmt.Exec():执行stmt准备好的 SQL 语句,适用于 INSERT、UPDATE、DELETE 等不返回结果集的操作。执行后返回sql.Result,其中:
res.LastInsertId()返回由数据库执行插入操作得到的自增 ID 号(对应 database/sql 接口 中driver.Result的LastInsertId);res.RowsAffected()返回执行 Update/Insert 等操作影响的数据条目数(对应driver.Result的RowsAffected)。
参数占位符与 SQL 注入防护
注意上面的代码中,所有传入 SQL 的数据都是通过?占位符传递的:INSERT ... SET username=?,department=?,created=?、UPDATE ... SET username=? WHERE uid=?、DELETE ... WHERE uid=?,再在stmt.Exec(...)中按顺序传入对应的值。这种方式可以一定程度上防止 SQL 注入:预编译语句会把参数作为数据而非 SQL 片段处理,恶意输入(如' OR '1'='1)不会被拼接到语句中执行。
对比直接字符串拼接:
// 危险:存在 SQL 注入风险 db.Query("SELECT * FROM userinfo WHERE username='" + userInput + "'")使用占位符的写法(如上文示例)是更安全的选择。同时,?的个数必须与传入参数一一对应;database/sql 接口 中提到,driver.Stmt的NumInput()返回预留参数的个数,当返回值>= 0时驱动会智能检查调用者传入的参数是否匹配。
database/sql 连接池:长连接是如何维持的
除了驱动自身的 keepalive,database/sql内部还实现了一个连接池,这是 MySQL 长连接得以高效复用的关键。database/sql 接口 中展示了DB结构的核心字段:
type DB struct { driver driver.Driver dsn string mu sync.Mutex // protects freeConn and closed freeConn []driver.Conn closed bool }其中freeConn就是那个简易的连接池。它的实现相当简单:当执行db.prepare→db.prepareDC时会defer dc.releaseConn,然后调用db.putConn把连接放回连接池;每次调用db.conn时,先判断freeConn的长度是否大于 0,大于 0 说明有可以复用的连接,直接拿出来用;否则创建一个新连接再返回。因此,即使你的业务代码反复sql.Open或频繁执行 SQL,底层也会尽量复用已建立的 MySQL 连接,配合 go-sql-driver/mysql 底层的 keepalive 支持,长连接的稳定性更有保障。
由于驱动已经按照database/sql的建议实现了连接池,你不用再自行实现缓存连接,否则反而容易引起问题。
运行与验证
在完成 MySQL 安装、建库建表之后,按以下步骤运行示例:
# 1. 获取第三方驱动依赖 go get github.com/go-sql-driver/mysql # 2. 运行程序(确保账号密码与 DSN 中的配置一致) go run main.go预期输出大致为:
id of last inserted row = 1 1 row(s) changed uid | username | department | created 1 | astaxieupdate | 研发部门 | 2012-12-09 1 row(s) changed可以看到:插入返回自增 ID,更新返回受影响行数,查询逐行打印出最新数据,删除后同样返回受影响行数。如果账号、库名或表结构不正确,程序会在checkErr处panic并打印错误信息,这也是示例用统一checkErr包装所有错误调用的目的——一旦某一步失败立即暴露问题。
小结与延伸阅读
本节以 go-sql-driver/mysql 为例,走完了 Go 使用 MySQL 的完整链路:驱动选型 → 建表 → DSN 连接 → Prepare/Exec 增删改查 → 结果解析。核心要点有三:
- 使用支持
database/sql的驱动(如 go-sql-driver/mysql),业务代码不绑定具体数据库,便于未来迁移; - 通过
?占位符 +stmt.Exec传入参数,既高效又可防 SQL 注入; database/sql内部自带简单连接池,配合驱动 keepalive,长连接使用更安全稳定。
对比阅读 使用 SQLite 数据库 一节可以发现:SQLite 示例的 CRUD 代码与本节几乎一模一样,唯一改变的是导入的驱动和sql.Open的连接串——这正是database/sql标准接口"一次编写、多处迁移"价值的直观体现。后续还可以继续学习 使用 PostgreSQL 数据库 与 使用 Redis 等章节,进一步掌握 Go 中多数据源的接入方式。
相关参考:
- 第 5 章目录
- 上一节:database/sql 接口
- 下一节:使用 SQLite 数据库
- 配套示例:en/code/src/apps/ch.5.2/main.go、en/code/src/apps/ch.5.2/schema.sql、en/code/src/apps/ch.5.2/readme.md
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
使用 Go database/sql 操作 MySQL:驱动选型、DSN 配置与 CRUD 实战(build-web-application-with-golang 第 5.2 节详解)
使用 Go database/sql 操作 MySQL:驱动选型、DSN 配置与 CRUD 实战(build web application with gola
文档教程Go 中使用 MySQL 数据库:基于 database/sql 的驱动选择与 CRUD 实战(Build Web Application with Golang 第 5.2 节)
Go 中使用 MySQL 数据库:基于 database/sql 的驱动选择与 CRUD 实战(Build Web Application with Golan
文档教程Build Web Application with Golang 第 5.2 节:使用 Go 的 MySQL 驱动实现 database/sql 增删改查实战
Build Web Application with Golang 第 5.2 节:使用 Go 的 MySQL 驱动实现 database/sql 增删改查实战
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考