使用 Golang 访问关系型数据库(译)
本教程介绍了使用 Go 和 Go 标准库中的 database/sql
访问关系型数据库的基础知识.
如果你对 Go 和它的工具有基本的了解, 那么本篇教程将可以发挥它的最大功效. 如果你是第一次接触 Go, 请阅读 Tutorial: Get started with Go, 快速地了解一下 Go.
你即将使用的 database/sql
包中, 包含了一些数据类型和函数, 可以用于连接数据库、执行会话、取消正在执行的操作, 等等. 关于使用这个包的更多细节, 请见Accessing databases.
在本篇教程中, 你将要创建一个数据库, 然后编写代码来对这个数据库进行访问. 你将要完成的示例项目, 是一个关于复古爵士乐的曲库.
在本篇教程中, 你将会按照以下几个小节, 循序渐进地进行学习:
- 为你的代码创建目录
- 设置一个数据库
- 导入数据库驱动
- 获取一个数据库句柄并进行连接
- 查询多行记录
- 查询单行记录
- 添加数据
Note: 更多其他教程, 请见 Tutorials
前提条件
- 已安装 MySQL 关系型数据库管理系统(DBMS).
- 已安装 Go. 安装说明, 请见Installing Go.
- 代码编辑器. 任何你已有的文本编辑器都可以满足需求.
- 命令行终端. Go 可以在 Linux 或 Mac 终端正常工作, 也可以在 Windows 的 PowerShell 或 cmd 中运行.
为你的代码创建目录
为了开始编写代码, 你需要先创建一个目录.
1.打开命令行, 进入到 Home 目录
在 Linux 或者 Mac 上执行如下命令:
1 | $ cd |
或在 Windows 上执行以下命令:
1 | C:\> cd %HOMEPATH% |
下文将仅使用 $
表示命令提示符. 所有命令在 Windows 系统也都同样可以运行.
2.在命令行提示符后面输入命令, 为你的代码创建一个名为 data-access 的目录.
1 | $ mkdir data-access |
3.创建一个模块, 用来管理你在学习本教程的过程中, 将要添加到项目中的依赖.
运行 go mod init
命令, 并使用一个你的新代码要用的模块路径(module path)作为参数.
1 | $ go mod init example/data-access |
这个命令创建了一个 go.mod 文件用于追踪依赖, 你添加的所有依赖都将会列在这个文件中. 更多内容, 请务必阅读Managing dependencies
Note: 在实际的开发中, 最好根据你的需求制定一个更具体的模块路径(module path). 更多详情, 请见Managing dependencies
设置一个数据库
在这一步, 你将会创建一个需要用到的数据库. 你将使用 DBMS 的 CLI 直接创建数据库和数据表, 并使用 CLI 添加数据.
你将会创建一个保存黑胶唱片上关于复古爵士乐数据的数据库.
这里的代码使用 MySQL CLI, 但是大多数 DBMS 都有它们自己的 CLI, 并且拥有相似的功能.
1.打开一个新的命令提示符
2.在命令行中, 登录你的 DBMS, MySQL 的示例如下:
1 | $ mysql -u -p |
3.在 mysql 的命令提示符中, 创建一个数据库:
1 | mysql> create datebase recordings; |
4.切换到刚刚添加的数据库中, 以便新增数据表.
1 | mysql> use recordings; |
5.打开文本编辑器, 在 data-access 文件夹中, 创建一个名为 create-tables.sql 的文件, 用于保存创建数据表的 SQL 语句.
6.打开这个文件, 粘贴如下 SQL 代码并保存:
1 | DROP TABLE IF EXISTS album; |
在上述 SQL 代码中, 你进行了以下几项操作:
- 删除(drop)名为
album
的数据表. 首先执行这个命令主要是为了在日后重新创建这个数据表时, 可以更简单地重新运行这个脚本. - 创建一个
album
表, 包含四个列:title
,artist
,price
和id
. 每一行的id
列由 DBMS 自动创建. - 添加四行数据.
7.在 mysql
命令提示符中, 运行你刚刚创建的脚本:
你将按照以下格式使用 source
命令:
1 | mysql> source /path/to/create-tables.sql |
8.在你的 DBMS 命令提示符中, 使用 SELECT
语句来验证你成功创建的数据表和数据.
1 | mysql> select * from album; |
下一步, 你将编写一些 Go 代码来连接数据库, 以便进行数据查询.
查找并导入数据库驱动
现在你有一个数据库, 并且里面存储了一些数据. 下面开始编写你的 Go 代码.
找出并导入一个数据库驱动, 你通过 database/sql
库的函数创建的请求, 将会被翻译成为数据库可以理解的请求.
1.在浏览器中, 打开 SQLDrivers wiki 页面, 找到一个你可以使用的驱动.
从这个页面中的列表找到一个你要用的驱动. 本教程中你将会使用 Go-MySQL-Driver 来访问数据库.
2.注意这里用到的驱动包名, github.com/go-sql-driver/mysql
.
3.使用你的文本编辑器, 创建一个名为 main.go 的文件用来编写 Go 代码, 并保存在你之前创建的 data-access 目录中.
4.在 main.go 中, 粘贴如下代码导入驱动包:
1 | package main |
在这段代码中, 你进行了以下操作:
- 将你的代码添加到
main
包中, 以便它可以独立运行. - 导入 MySQL 驱动
github.com/go-sql-driver/mysql
导入驱动后, 你将要开始编写代码来访问数据库.
获取一个数据库句柄并进行连接
现在来编写一些代码, 通过一个数据库句柄, 让你获得数据库的入口.
你将要使用一个指向 sql.DB
结构体的指针, 来表示一个指定数据库的入口.
编写代码
1.打开 main.go 文件, 在你刚刚添加的 import
代码的下方, 粘贴如下 Go 代码来创建一个数据库句柄:
1 | var db *sql.DB |
在这段代码中, 你进行了以下几个操作:
- 声明了一个
*sql.DB
类型的db
变量. 这就是你的数据库句柄.
把 db
变量作为全局变量, 简化了这个示例. 但是在生产代码中, 你应该避免使用全局变量, 例如将变量作为参数传入函数, 或者将其包装在一个结构体内.
- 使用 MySQL 驱动的
Config
和FormatDSN
, 把连接的属性集中在一起, 并将他们格式化成 DSN (Database Source Name) 格式, 成为一个连接字符串.
相比于一个连接字符串, Config
结构体使得代码的可读性更强.
调用
sql.Open
来初始化db
变量, 传入FormatDSN
的返回值.检查
sql.Open
返回的异常.sql.Open
有可能会失败, 比如, 当你的数据库连接属性不符合规范的时候.
为了简化代码, 你调用了 log.Fatal
来终止代码的运行, 并向控制台输出异常信息. 在生产代码中, 你可能会希望用更平滑的方式来处理异常.
调用
DB.Ping
以确认和数据库的连接是否正常. 在运行时,sql.Open
可能不会立即建立连接, 这取决于驱动. 你在这里使用Ping
来确认确保database/sql
包在需要的时候可以连接到数据库.检查
Ping
返回的异常, 假如连接失败的话.如果
Ping
连接成功, 打印一条信息.
2.在 main.go 文件的顶部, 紧挨着 package 声明的下方, 导入所有你在刚刚编写的那段代码中用到的包.
编辑后的文件顶部代码如下:
1 | package main |
3.保存 main.go
运行代码
- 将 MySQL 驱动模块作为依赖进行追踪.
使用 go get
命令来把 github.com/go-sql-driver/mysql 模块作为你自己模块的依赖添加进来. 使用 .
参数的意思是“获取当前目录中代码的依赖”.
1 | $ go get . |
Go 完成了对这个依赖的下载, 是因为你在之前的步骤中把它添加到了 import
声明里. 更多关于依赖跟踪的的内容, 请见 Adding a dependency.
2.在命令提示符界面, 设置 Go 程序需要用到的 DBUSER
和 DBPASS
环境变量.
在 Linux 或 Mac 系统上执行:
1 | $ export DBUSER=username |
在 Windows 系统上执行:
1 | C:\Users\you\daa-access> set DBUSER=username |
3.在命令行中, 包含 main.go 的目录下, 输入 go run
命令和点参数(.
)来运行代码. 这行命令的含义是“在当前目录下运行这个包”.
1 | $ go run . |
你可以连接数据库了! 下一步, 你将要完成对一些数据的查询.
查询多行记录
在这一小节, 你讲使用 Go 来运行一条查询并返回多行数据的 SQL 语句.
对于可能返回多行数据的 SQL 语句, 你使用 database/sql
包中的 Query
方法, 然后遍历返回的数据行.(你稍后会在 Query for a single row 中学习如何查询单行数据.)
编写代码
1.打开 main.go 文件, 在紧挨着 func main
的上方, 粘贴如下代码, 定义一个 Album
结构体. 你将使用这个结构体来储存查询返回的数据行.
1 | type Album struct { |
2.在 func main
的下方, 粘贴下面的 albumsByArtist
函数用于查询数据库.
1 | // albumsByArtist 用来查询拥有指定艺术家名称的专辑 |
在这段代码中, 你完成了以下几项操作:
声明了一个你定义的
Album
类型的切片变量albums
. 这个切片将用来保存返回的数据行中的数据. 结构体字段名称和类型与数据库列的名称以及类型相对应.使用
DB.Query
执行SELECT
语句, 对指定艺术家名称的专辑进行查询.
Query
的第一个参数是 SQL 语句. 在这个参数的后面, 你可以传入零个或多个任意类型的参数. 这给你提供了一个可以为 SQL 语句指定参数值的位置. 通过将 SQL 语句和参数值拆分开 (而不是使用 fmt.Printf
拼接在一起), database/sql
包可以将 SQL 语句文本和值分开传递, 避免了 SQL 注入的风险.
使用 Defer 关闭
rows
, 使它持有的资源 可以在函数退出时释放.遍历返回的全部数据行, 使用
Rows.Scan
来将每个数据行中各列的数据赋值到Album
结构体的字段中.
Scan
接收一系列 Go 值的指针, 各列的值将会写入这些指针指向的位置. 在这里, 你使用 &
操作符创建了指向 alb
变量中不同字段的指针, 并将它们传入 Scan
中. Scan
通过这些指针写入数据, 对结构体的字段进行更新.
在循环中, 校验列数据赋值到结构体字段时返回的异常.
在循环中, 把新的
alb
追加到albums
切片中.在循环之后, 使用
rows.Err
校验整个查询过程中的异常, 注意如果查询本身失败了, 而想要发现结果不完整, 在这里进行异常校验是唯一的方式.
3.更新你的 main
函数, 对 albumsByArtist
进行调用.
向 func main
的末尾, 添加如下代码:
1 | albums, err := albumsByArtist("John Coltrane") |
在新增的代码中, 你现在完成了以下操作:
- 调用你添加的
albumsByArtist
函数, 将返回值赋值到新的albums
变量上. - 打印结果
运行代码
打开命令行, 在 main.go 所在的目录下, 运行代码.
1 | $ go run . |
下一步, 你将会查询单行数据.
查询单行记录
在本小节, 你将使用 Go 来完成对数据库中单行数据的查询.
对于你已知的最多只会返回一个数据行的 SQL 语句, 你可以使用 QueryRow
, 这样比使用 Query
循环更加简单.
编写代码
1.在 albumsByArtist
的下方, 粘贴 albumByID
函数代码如下:
1 | // albumByID 通过指定 ID 查询专辑 |
在这段代码中, 你完成了以下操作:
- 使用
DB.QueryRow
执行了SELECT
语句, 根据指定的 ID 对专辑进行查询.
DB.QueryRow
返回了一个 sql.Row
. 为了简化你的代码, QueryRow
并没有返回异常. 而是把查询异常(比如 sql.ErrNoRows
)放到了 Rows.Scan
后面.
使用
Row.Scan
将每列的值赋值到结构体字段中校验
Scan
返回的异常
这个特殊的 sql.ErrNoRows
异常表示查询没有任何一条结果返回. 这个异常通常有必要使用更明确的文本信息替代, 比如这里的 “no such album”.
2.更新 main
函数, 对 albumByID
进行调用
向 func main
的末尾, 添加如下代码:
1 | // 在这里硬编码 ID 为 2 用于查询测试 |
在这段新增代码中, 你完成了以下操作:
- 调用了你添加的
albumByID
函数 - 打印了这个专辑 ID 的返回值
运行代码
打开命令行, 在 main.go 所在的目录, 运行代码.
1 | $ go run . |
下一步, 你将会添加一条专辑信息到数据库中.
添加数据
在本小节, 你将使用 Go 执行 SQL 的 INSERT
语句, 来添加一条新数据到数据库中.
你已经了解到了如何使用 Query
和 QueryRow
两个有返回数据的函数来执行 SQL 语句. 那么对于不返回数据的 SQL 语句, 你需要使用 Exec
编写代码
1.在 albumByID
函数的下方, 粘贴 addAlbum
函数的代码如下, 用于添加一条新的专辑数据到数据库中, 然后保存 main.go 文件.
1 | // addAlbum 添加指定的专辑信息到数据库中 |
在这段代码中, 你完成了以下操作:
- 使用
DB.Exec
执行了INSERT
语句
就像Query
一样,Exec
接收 SQL 语句和这个 SQL 语句的参数值. - 校验尝试执行
INSERT
语句时返回的异常 - 使用
Result.LastInsertId
取回新插入数据的 ID. - 校验尝试取回 ID 时返回的异常.
2.更新 main
函数对新增的 addAlbum
函数进行调用.
向 func main
的末尾, 添加如下代码:
1 | albID, err := addAlbum(Album{ |
在这段新增代码中, 你现在完成了以下操作:
- 调用
addAlbum
并使用一个新专辑信息作为参数, 将你添加的专辑信息 ID 赋值给一个albID
变量.
运行代码
打开命令行, 在 main.go 所在的目录下, 运行代码.
1 | $ go run . |
总结
恭喜你! 你刚刚使用 Go 完成了一些对关系型数据库的简单操作.
接下来的一些建议:
- 阅读一下数据存取指南(data access guide), 有一些本教程仅仅浅尝辄止的内容, 在数据存取指南中有更详细的介绍.
- 如果你刚刚开始使用 Go, 你可以在 Effective Go 和 How to wirte Go code 中找到非常有用的 Go 语言最佳实践.
- Go Tour 是一个非常好的入门教程, 循序渐进地介绍了 Go 语言的基础.
完整代码
本小节包含了你跟随本教程构建的程序的完整代码.
1 | package main |