news 2026/10/24 11:45:34

使用 Go 的 database/sql 接口操作 MySQL:从驱动选型到 CRUD 实战(build-web-application-with-golang 第 5.2 节)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Go 的 database/sql 接口操作 MySQL:从驱动选型到 CRUD 实战(build-web-application-with-golang 第 5.2 节)
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-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)建议的步骤如下:

  1. 安装并运行 MySQL;
  2. 按照main.go中的常量创建用户与数据库:
const ( DB_USER = "user" DB_PASSWORD = "" DB_NAME = "test" )
  1. 执行 schema.sql 创建userinfo表;
  2. 执行go get下载并安装远程依赖包;
  3. 用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=utf8Unix 域套接字通过本地 socket 文件连接,常用于本机 MySQL
user:password@tcp(localhost:5555)/dbname?charset=utf8TCP指定主机与端口,默认端口为 3306
user:password@/dbname默认协议不显式写协议时使用驱动默认的本地连接方式
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbnameIPv6 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 增删改查 → 结果解析。核心要点有三:

  1. 使用支持database/sql的驱动(如 go-sql-driver/mysql),业务代码不绑定具体数据库,便于未来迁移;
  2. 通过?占位符 +stmt.Exec传入参数,既高效又可防 SQL 注入;
  3. 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

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 16:33:06

选具备正规医疗器械注册证的家用康复理疗品牌指南

选具备正规医疗器械注册证的家用康复理疗品牌指南在面对琳琅满目的康复设备时,消费者应优先关注产品的合规性。审慎判断显示,选择持有二类或一类医疗器械注册证的产品是降低使用风险的基础。只有经过药监部门审批的设备,其安全性和有效性才具…

作者头像 李华
网站建设 2026/10/23 22:55:56

Codex桌面版更新后无法加载组织设置?Windows排查修复指南

1. 问题现象与排查思路总览Codex 桌面版在 Windows 上更新之后直接打不开,启动时弹出一句「无法加载组织设置」,然后窗口一闪就没了。这个现象我最近帮人处理过好几次,症状几乎一模一样:更新前好好的,更新完就废&#…

作者头像 李华
网站建设 2026/10/15 18:42:15

word文件内存大怎么变小?3种实用方法快速搞定

52页的技术方案文档,2万8千字,里面塞了40多张截图,另存之后一看——86MB。 合租室友小赵半夜给我发了条语音方阵,连发七八条,每条开头都是"哥你睡了吗",最后一条才说清楚:这文档明天早…

作者头像 李华