使用 Golang 访问关系型数据库(译)

原文: https://go.dev/doc/tutorial/database-access

本教程介绍了使用 Go 和 Go 标准库中的 database/sql 访问关系型数据库的基础知识.

如果你对 Go 和它的工具有基本的了解, 那么本篇教程将可以发挥它的最大功效. 如果你是第一次接触 Go, 请阅读 Tutorial: Get started with Go, 快速地了解一下 Go.

你即将使用的 database/sql 包中, 包含了一些数据类型和函数, 可以用于连接数据库、执行会话、取消正在执行的操作, 等等. 关于使用这个包的更多细节, 请见Accessing databases.

在本篇教程中, 你将要创建一个数据库, 然后编写代码来对这个数据库进行访问. 你将要完成的示例项目, 是一个关于复古爵士乐的曲库.

在本篇教程中, 你将会按照以下几个小节, 循序渐进地进行学习:

  1. 为你的代码创建目录
  2. 设置一个数据库
  3. 导入数据库驱动
  4. 获取一个数据库句柄并进行连接
  5. 查询多行记录
  6. 查询单行记录
  7. 添加数据

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
2
$ mkdir data-access
$ cd data-access

3.创建一个模块, 用来管理你在学习本教程的过程中, 将要添加到项目中的依赖.

运行 go mod init 命令, 并使用一个你的新代码要用的模块路径(module path)作为参数.

1
2
$ go mod init example/data-access
go: createing new go.mod: module example/data-access

这个命令创建了一个 go.mod 文件用于追踪依赖, 你添加的所有依赖都将会列在这个文件中. 更多内容, 请务必阅读Managing dependencies

Note: 在实际的开发中, 最好根据你的需求制定一个更具体的模块路径(module path). 更多详情, 请见Managing dependencies


设置一个数据库

在这一步, 你将会创建一个需要用到的数据库. 你将使用 DBMS 的 CLI 直接创建数据库和数据表, 并使用 CLI 添加数据.

你将会创建一个保存黑胶唱片上关于复古爵士乐数据的数据库.

这里的代码使用 MySQL CLI, 但是大多数 DBMS 都有它们自己的 CLI, 并且拥有相似的功能.

1.打开一个新的命令提示符
2.在命令行中, 登录你的 DBMS, MySQL 的示例如下:

1
2
3
4
$ mysql -u -p
Enter password:

mysql>

3.在 mysql 的命令提示符中, 创建一个数据库:

1
mysql> create datebase recordings;

4.切换到刚刚添加的数据库中, 以便新增数据表.

1
2
mysql> use recordings;
Database changed

5.打开文本编辑器, 在 data-access 文件夹中, 创建一个名为 create-tables.sql 的文件, 用于保存创建数据表的 SQL 语句.

6.打开这个文件, 粘贴如下 SQL 代码并保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP TABLE IF EXISTS album;
CREATE TABLE album (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(128) NOT NULL,
artist VARCHAR(255) NOT NULL,
price DECIMAL(5,2) NOT NULL,
PRIMARY KEY (`id`)
);

INSERT INTO album
(title, artist, price)
VALUES
('Blue Train', 'John Coltrane', 56.99),
('Giant Steps', 'John Coltrane', 63.99),
('Jeru', 'Gerry Mulligan', 17.99),
('Sarah Vaughan', 'Sarah Vaughan', 34.98);

在上述 SQL 代码中, 你进行了以下几项操作:

  • 删除(drop)名为 album 的数据表. 首先执行这个命令主要是为了在日后重新创建这个数据表时, 可以更简单地重新运行这个脚本.
  • 创建一个 album 表, 包含四个列: title, artist, priceid. 每一行的 id 列由 DBMS 自动创建.
  • 添加四行数据.

7.在 mysql 命令提示符中, 运行你刚刚创建的脚本:

你将按照以下格式使用 source 命令:

1
mysql> source /path/to/create-tables.sql

8.在你的 DBMS 命令提示符中, 使用 SELECT 语句来验证你成功创建的数据表和数据.

1
2
3
4
5
6
7
8
9
10
mysql> select * from album;
+----+---------------+----------------+-------+
| id | title | artist | price |
+----+---------------+----------------+-------+
| 1 | Blue Train | John Coltrane | 56.99 |
| 2 | Giant Steps | John Coltrane | 63.99 |
| 3 | Jeru | Gerry Mulligan | 17.99 |
| 4 | Sarah Vaughan | Sarah Vaughan | 34.98 |
+----+---------------+----------------+-------+
4 rows in set (0.00 sec)

下一步, 你将编写一些 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
2
3
package main

import "github.com/go-sql-driver/mysql"

在这段代码中, 你进行了以下操作:

  • 将你的代码添加到 main 包中, 以便它可以独立运行.
  • 导入 MySQL 驱动 github.com/go-sql-driver/mysql

导入驱动后, 你将要开始编写代码来访问数据库.


获取一个数据库句柄并进行连接

现在来编写一些代码, 通过一个数据库句柄, 让你获得数据库的入口.

你将要使用一个指向 sql.DB 结构体的指针, 来表示一个指定数据库的入口.

编写代码

1.打开 main.go 文件, 在你刚刚添加的 import 代码的下方, 粘贴如下 Go 代码来创建一个数据库句柄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var db *sql.DB

func main() {
cfg := mysql.Config{
User: os.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
}
// Get a database handle
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}

pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
}

在这段代码中, 你进行了以下几个操作:

  • 声明了一个 *sql.DB 类型的 db 变量. 这就是你的数据库句柄.

db 变量作为全局变量, 简化了这个示例. 但是在生产代码中, 你应该避免使用全局变量, 例如将变量作为参数传入函数, 或者将其包装在一个结构体内.

  • 使用 MySQL 驱动的 ConfigFormatDSN , 把连接的属性集中在一起, 并将他们格式化成 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
2
3
4
5
6
7
8
9
10
package main

import (
"database/sql"
"fmt"
"log"
"os"

"github.com/go-sql-driver/mysql"
)

3.保存 main.go

运行代码

  1. 将 MySQL 驱动模块作为依赖进行追踪.

使用 go get 命令来把 github.com/go-sql-driver/mysql 模块作为你自己模块的依赖添加进来. 使用 . 参数的意思是“获取当前目录中代码的依赖”.

1
2
$ go get .
go get: added github.com/go-sql-driver/mysql v1.6.0

Go 完成了对这个依赖的下载, 是因为你在之前的步骤中把它添加到了 import 声明里. 更多关于依赖跟踪的的内容, 请见 Adding a dependency.

2.在命令提示符界面, 设置 Go 程序需要用到的 DBUSERDBPASS 环境变量.

在 Linux 或 Mac 系统上执行:

1
2
$ export DBUSER=username
$ export DBPASS=password

在 Windows 系统上执行:

1
2
C:\Users\you\daa-access> set DBUSER=username
C:\Users\you\daa-access> set DBPASS=password

3.在命令行中, 包含 main.go 的目录下, 输入 go run 命令和点参数(.)来运行代码. 这行命令的含义是“在当前目录下运行这个包”.

1
2
$ go run .
Connected!

你可以连接数据库了! 下一步, 你将要完成对一些数据的查询.


查询多行记录

在这一小节, 你讲使用 Go 来运行一条查询并返回多行数据的 SQL 语句.

对于可能返回多行数据的 SQL 语句, 你使用 database/sql 包中的 Query 方法, 然后遍历返回的数据行.(你稍后会在 Query for a single row 中学习如何查询单行数据.)

编写代码

1.打开 main.go 文件, 在紧挨着 func main 的上方, 粘贴如下代码, 定义一个 Album 结构体. 你将使用这个结构体来储存查询返回的数据行.

1
2
3
4
5
6
type Album struct {
ID int64
Title string
Artist string
Price float32
}

2.在 func main 的下方, 粘贴下面的 albumsByArtist 函数用于查询数据库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// albumsByArtist 用来查询拥有指定艺术家名称的专辑
func albumsByAritist(name string) ([]Album, error) {
// 一个用来保存返回的数据行的 albums slice
var albums []Album

rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
defer rows.Close()
// 遍历数据行, 使用 Scan 来将每列的数据赋值到结构体的字段上.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByAritst %q: %v", name, err)
}
return albums, nil
}

在这段代码中, 你完成了以下几项操作:

  • 声明了一个你定义的 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
2
3
4
5
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)

在新增的代码中, 你现在完成了以下操作:

  • 调用你添加的 albumsByArtist 函数, 将返回值赋值到新的 albums 变量上.
  • 打印结果

运行代码

打开命令行, 在 main.go 所在的目录下, 运行代码.

1
2
3
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]

下一步, 你将会查询单行数据.


查询单行记录

在本小节, 你将使用 Go 来完成对数据库中单行数据的查询.

对于你已知的最多只会返回一个数据行的 SQL 语句, 你可以使用 QueryRow, 这样比使用 Query 循环更加简单.

编写代码

1.在 albumsByArtist 的下方, 粘贴 albumByID 函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// albumByID 通过指定 ID 查询专辑
func albumByID(id int64) (Album, error) {
// 保存查询返回的专辑数据
var alb Album

row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %v", id, err)
}
return alb, nil
}

在这段代码中, 你完成了以下操作:

  • 使用 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
2
3
4
5
6
// 在这里硬编码 ID 为 2 用于查询测试
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)

在这段新增代码中, 你完成了以下操作:

  • 调用了你添加的 albumByID 函数
  • 打印了这个专辑 ID 的返回值

运行代码

打开命令行, 在 main.go 所在的目录, 运行代码.

1
2
3
4
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}

下一步, 你将会添加一条专辑信息到数据库中.


添加数据

在本小节, 你将使用 Go 执行 SQL 的 INSERT 语句, 来添加一条新数据到数据库中.

你已经了解到了如何使用 QueryQueryRow 两个有返回数据的函数来执行 SQL 语句. 那么对于不返回数据的 SQL 语句, 你需要使用 Exec

编写代码

1.在 albumByID 函数的下方, 粘贴 addAlbum 函数的代码如下, 用于添加一条新的专辑数据到数据库中, 然后保存 main.go 文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
// addAlbum 添加指定的专辑信息到数据库中
// 返回新数据到专辑 ID
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
return id, nil
}

在这段代码中, 你完成了以下操作:

  • 使用 DB.Exec 执行了 INSERT 语句
    就像 Query 一样, Exec 接收 SQL 语句和这个 SQL 语句的参数值.
  • 校验尝试执行 INSERT 语句时返回的异常
  • 使用 Result.LastInsertId 取回新插入数据的 ID.
  • 校验尝试取回 ID 时返回的异常.

2.更新 main 函数对新增的 addAlbum 函数进行调用.

func main 的末尾, 添加如下代码:

1
2
3
4
5
6
7
8
9
albID, err := addAlbum(Album{
Title: "The modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)

在这段新增代码中, 你现在完成了以下操作:

  • 调用 addAlbum 并使用一个新专辑信息作为参数, 将你添加的专辑信息 ID 赋值给一个 albID 变量.

运行代码

打开命令行, 在 main.go 所在的目录下, 运行代码.

1
2
3
4
5
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
ID of added album: 5


总结

恭喜你! 你刚刚使用 Go 完成了一些对关系型数据库的简单操作.

接下来的一些建议:

  • 阅读一下数据存取指南(data access guide), 有一些本教程仅仅浅尝辄止的内容, 在数据存取指南中有更详细的介绍.
  • 如果你刚刚开始使用 Go, 你可以在 Effective GoHow to wirte Go code 中找到非常有用的 Go 语言最佳实践.
  • Go Tour 是一个非常好的入门教程, 循序渐进地介绍了 Go 语言的基础.


完整代码

本小节包含了你跟随本教程构建的程序的完整代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main

import (
"database/sql"
"fmt"
"log"
"os"

"github.com/go-sql-driver/mysql"
)

var db *sql.DB

type Album struct {
ID int64
Title string
Artist string
Price float32
}

func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}

pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")

albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)

// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)

albID, err := addAlbum(Album{
Title: "The Modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)
}

// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
// An albums slice to hold data from returned rows.
var albums []Album

rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
defer rows.Close()
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
return albums, nil
}

// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
// An album to hold data from the returned row.
var alb Album

row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %v", id, err)
}
return alb, nil
}

// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
return id, nil
}