☰
build-web-application-with-golang 第 5.2 章实战:用 Go database/sql 驱动 MySQL 完成增删改查
2026/10/3 8:41:29 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】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
点击查看免费下载

本章实战围绕仓库中 en/code/src/apps/ch.5.2 目录下的可运行示例展开。该示例与 《Build Web Application with Golang》第 5.2 章 "MySQL" 一一对应,演示了如何基于 Go 标准库database/sql接口 +github.com/go-sql-driver/mysql纯 Go 驱动,对 MySQL 执行插入、更新、查询、删除四类核心操作。读完本文,你将掌握这套示例从「安装 MySQL → 建库建表 → 拉取依赖 → 编译运行」的完整落地流程,并能读懂database/sql中sql.Open、db.Prepare、stmt.Exec、rows.Scan等关键 API 的底层行为与正确用法。

一、示例在全书中的定位

第 5 章讨论的是 Go 的数据库访问层。在 5.1 节 "database/sql interface" 中,作者详细剖析了 Go 标准库database/sql与database/sql/driver的接口体系:驱动通过sql.Register(name, driver)在init()中注册自己,driver.Driver、driver.Conn、driver.Stmt、driver.Tx、driver.Result、driver.Rows等接口共同构成了驱动与上层代码的解耦层。正因为上层代码只依赖database/sql标准接口,5.2、5.3、5.4、5.5 各节可以用完全相同的调用模式去对接 MySQL、SQLite、PostgreSQL 等不同数据库——这正是本示例值得动手跑一遍的原因:它是验证「接口标准化 + 驱动可插拔」这一设计的最佳最小实验。

本示例的目录 en/code/src/apps/ch.5.2 由三个文件组成:

文件作用
main.go完整的 CRUD 示例程序(可编译、可运行)
schema.sqluserinfo表的建表语句
readme.md运行本示例的 5 步设置指南

二、运行准备:5 步设置指南逐条拆解

readme.md 给出的设置流程非常精炼,下面结合仓库实际文件逐条展开,保证任何环境都能照做。

Step 1:安装并启动 MySQL

示例程序通过 TCP 连接 MySQL 服务端。在 Debian/Ubuntu 系可直接:

sudo apt-get install mysql-server sudo service mysql start

macOS 可用 Homebrew:

brew install mysql brew services start mysql

安装完成后可用mysql -u root -p验证服务已就绪。示例本身对 MySQL 版本没有特殊要求,标准 5.x 与 8.x 均可。

Step 2:按 main.go 中的常量创建用户与数据库

示例把连接所需的账号信息直接写成常量,见 main.go:

const ( DB_USER = "user" DB_PASSWORD = "" DB_NAME = "test" )

这三个常量对应关系如下:

常量值含义
DB_USERuserMySQL 登录用户名
DB_PASSWORD空字符串密码(示例默认无密码)
DB_NAMEtest目标数据库名

也就是说,你需要保证 MySQL 中存在一个用户名为user、密码为空的账号,并且该账号对名为test的数据库有读写权限。若你的环境与之不同,直接修改这三个常量即可,无需改动任何业务代码——这是该示例刻意把「环境差异」收敛到常量层的设计。

以 root 身份执行:

CREATE DATABASE test; CREATE USER 'user'@'localhost' IDENTIFIED BY ''; GRANT ALL PRIVILEGES ON test.* TO 'user'@'localhost'; FLUSH PRIVILEGES;

注意:示例 DSN 中未携带密码(user@/test形式),因此密码为空是满足运行条件的最简配置。生产环境请务必使用强密码,不要把凭证硬编码进源码。

Step 3:用 schema.sql 创建 userinfo 表

在 MySQL 客户端中执行建表脚本:

mysql -u user test < schema.sql

或进入交互模式后. source执行。建表语句位于 schema.sql:

CREATE TABLE `userinfo` ( `uid` INT(10) NOT NULL AUTO_INCREMENT, `username` VARCHAR(64) NULL DEFAULT NULL, `departname` VARCHAR(64) NULL DEFAULT NULL, `created` DATE NULL DEFAULT NULL, PRIMARY KEY (`uid`) );

表结构要点:

  • uid:主键,AUTO_INCREMENT自增,插入时无需显式赋值,程序通过LastInsertId()取回新行 ID;
  • username/departname:长度 64 的字符串列;
  • created:DATE类型,示例插入的是字符串"2012-12-09",MySQL 会按日期格式自动转换。

一个小提醒:示例代码 main.go 与 schema.sql 使用的是departname列名,而 5.2 章节正文 中的建表 SQL 写作department,二者并不一致。以仓库内可运行的 schema.sql 为准,二者择一保持一致即可,切勿混用导致 SQL 报错。

Step 4:运行 go get 安装远程依赖

示例只依赖一个第三方包:MySQL 驱动github.com/go-sql-driver/mysql。在项目目录下执行:

go get github.com/go-sql-driver/mysql

go get会把驱动源码下载到本地并可供编译链接。需要说明的是,仓库并未为示例提供go.mod或vendor目录,依赖管理方式取决于你使用的 Go 版本:

  • 使用 Go Modules(Go 1.11+ 默认):建议先go mod init ch5.2再执行go get,让go mod tidy生成go.mod/go.sum;
  • 使用传统GOPATH模式:可参考仓库根下 en/code/readme.md 的建议,把GOPATH指向代码目录,从而任意目录下都能正常 import。

Step 5:运行程序

一切就绪后,在 ch.5.2 目录 下执行:

go run main.go

程序会依次打印插入、更新、查询、删除四阶段的运行日志,具体输出含义见下一节源码解读。

三、main.go 源码逐段解读

main.go 完整演示了database/sql的标准用法,下面按执行顺序拆解。

3.1 导入与驱动注册

import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" )

注意_ "github.com/go-sql-driver/mysql"的空导入写法:如 5.1 节 所讲,Go 禁止导入后不使用的包,而空标识符导入的作用是只执行该包的init()而不直接引用其符号——go-sql-driver 正是利用init()调用sql.Register("mysql", ...)完成驱动注册。注册后,sql.Open("mysql", dsn)的第一个参数才能匹配到该驱动。

3.2 构造 DSN 并打开数据库句柄

dbSouce := fmt.Sprintf("%v:%v@/%v?charset=utf8", DB_USER, DB_PASSWORD, DB_NAME) db, err := sql.Open("mysql", dbSouce) checkErr(err) defer db.Close()

按前面常量展开,实际 DSN 为:

user:@/test?charset=utf8

5.2 章节正文 给出了database/sql驱动的 DSN 支持格式全集,除本示例使用的user:password@/dbname外,还包括:

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

可见@之前是认证信息,@之后可省略tcp(host:port)而走默认本地连接,也可显式指定 TCP 地址、Unix Socket 甚至 IPv6 地址;?charset=utf8是连接参数,保证客户端与 MySQL 之间的字符集一致。

需要特别澄清sql.Open的语义:它并不立即建立物理连接,只是初始化一个带连接池的DB句柄并校验 DSN 格式,真正的连接在首次执行 SQL 时才建立。db.Close()负责归还并关闭池中连接。从源码结构看,这正是 5.1 节 中database/sql包在DB结构上维护freeConn连接池(defer db.putConn(ci, err)回收连接、复用空闲连接)的高层封装。

3.3 插入:Prepare + Exec + LastInsertId

stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") checkErr(err) res, err := stmt.Exec("astaxie", "software developement", "2012-12-09") checkErr(err) id, err := res.LastInsertId() checkErr(err) fmt.Println("id of last inserted row =", id)

调用链为:db.Prepare让数据库预先编译 SQL 得到Stmt;stmt.Exec传入三个占位符参数执行插入;res.LastInsertId()取回自增主键uid的值,供后续 update/delete 使用。?占位符由驱动在客户端做参数化绑定,避免字符串拼接注入风险(详见本文第四节)。

3.4 更新:Exec + RowsAffected

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, "row(s) changed")

注意这里复用了同一个stmt变量(重新赋值),where uid=?中的?绑定到上一步取回的id。RowsAffected()返回本次 UPDATE 实际影响的行数,程序据此打印1 row(s) changed。

3.5 查询:Query + rows.Next/Scan

rows, err := db.Query("SELECT * FROM userinfo") checkErr(err) for rows.Next() { var uid int var username, department, created string err = rows.Scan(&uid, &username, &department, &created) checkErr(err) fmt.Println("uid | username | department | created") fmt.Printf("%3v | %6v | %6v | %6v\n", uid, username, department, created) }

与前面「先 Prepare 再 Exec」不同,db.Query直接执行 SELECT 并返回*sql.Rows结果集。惯用模式是:rows.Next()在循环中逐行推进,每次调用rows.Scan(&uid, &username, &department, &created)把当前行的各列按顺序扫描进对应变量地址。示例中局部变量名department与表列departname略有出入,但 Scan 是按列顺序取值的,与变量名无关。

按 5.1 节 对driver.Rows.Next的说明,行数据在驱动层会被转换为[]byte等driver.Value类型,再由Scan转换并填充到目标变量。

3.6 删除:Prepare + Exec + RowsAffected

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, "row(s) changed")

按插入时拿到的id精准删除该行,整个示例形成「插入 → 更新 → 查询 → 删除」的完整闭环。程序结束后defer db.Close()自动回收连接。

3.7 错误处理:checkErr

func checkErr(err error) { if err != nil { panic(err) } }

示例以panic快速暴露任何一步的错误(连接失败、SQL 语法错误、驱动缺失等),适合教程演示场景;真实业务中应改为返回 error 或记录日志,而不是让程序崩溃。

四、为什么必须用=?占位符

5.2 章节正文 特别强调:传参必须使用?占位符格式,这是防止 SQL 注入攻击的必要手段。示例中所有用户可控或动态生成的值(用户名、ID 等)都通过stmt.Exec(..., id)绑定,而不是拼进 SQL 字符串。驱动会在协议层对参数做转义与类型处理,恶意输入即使包含引号、分号也无法改变 SQL 结构。这是使用database/sql接口时的硬性安全规范,直接照搬本示例的写法即可获得正确且安全的默认行为。

五、预期运行输出

在环境正确的前提下运行go run main.go,输出大致为:

Inserting id of last inserted row = 1 Updating 1 row(s) changed Querying uid | username | department | created 1 | astaxieupdate | software developement | 2012-12-09 Deleting 1 row(s) changed

各阶段含义:插入返回新行自增 ID;更新报告影响 1 行;查询回显表中全部记录(此时应看到更新后的用户名);删除再次报告影响 1 行。若某一步panic,请按错误信息回查:驱动是否go get、MySQL 是否启动、user/test库是否创建、schema.sql 是否执行。

六、延伸:同一套代码跑多种数据库

本示例的骨架(Open → Prepare/Exec → Query/Scan)完全建立在database/sql标准接口之上,因此只需更换驱动与 DSN 即可移植到其他数据库。仓库中同章节的姊妹示例正好提供了对照:

  • ch.5.3(SQLite):使用sqlite3 foo.db建库、.read schema.sql建表,同样go get后go run main.go;
  • ch.5.4 / ch.5.5 目录(PostgreSQL、Redis)沿用同构流程;
  • 章节正文可参见 5.2 节(MySQL 驱动选型与 DSN 格式)与 5.1 节(database/sql接口体系与连接池原理)。

对比这些示例即可直观感受到「接口标准化、驱动可插拔」带来的迁移成本优势——业务代码几乎零改动。

七、小结与常见问题速查

本文从 readme.md 的 5 步设置出发,完整展开了 MySQL 示例的运行流程与 main.go 的 CRUD 实现,并对照 schema.sql 与 5.1 节、5.2 节 章节正文讲解了sql.Open、Prepare/Exec、RowsAffected、LastInsertId、Query/Scan等 API 的语义与底层行为。最后给出高频问题的快速排查表:

现象常见原因
panic: dial tcp ... connection refusedMySQL 未启动,或 DSN 未指定tcp(host:port)且服务不在默认 3306
panic: Access denied for user 'user'user账号未创建、密码不符或未授权test库
panic: driver: bad connection驱动未通过go get安装,空导入注册失败
Error 1146: Table 'test.userinfo' doesn't exist未执行 schema.sql
SQL 报列名不存在章节正文用department、示例代码用departname,请与 schema.sql 保持一致

按本文步骤走完一遍,你就拥有了一个可直接复用的 Go + MySQL CRUD 最小范例,可在此基础上平滑扩展为 Web 应用的数据库访问层。

  • 文档
  • 教程

【免费下载链接】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
点击查看免费下载
上一篇:NumPy 1.6.1 缺陷修复版本解析:einsum、nditer、f2py 与对象数组回归问题的源码级解读
下一篇:RocketMQ 生产者启动流程源码深度解析(基于 4.9.3 版本)

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询