本文翻译自《Tutorial: Accessing a relational database》。
目录
本教程介绍了使用Go及其标准库中的database/sql
包访问关系数据库的基础知识。
如果你对Go及其工具有基本的了解,你将充分利用本教程。如果这是你第一次接触Go,请先参阅教程:Go入门以获得快速介绍。
你将使用的database/sql
包包括用于连接数据库、执行事务、取消正在进行的操作等的类型和函数。有关使用该包的更多详细信息,请参阅访问关系型数据库。
在本教程中,你将创建一个数据库,然后编写代码来访问该数据库。你的示例项目将是有关古典爵士乐唱片的数据存储库。
在本教程中,你将逐步完成以下部分:
1 为你的代码创建一个文件夹。
2 建立一个数据库。
3 导入数据库驱动。
4 获取数据库句柄并连接。
5 查询多行。
6 查询单行。
7 添加数据。
注意:有关其他教程,请参阅教程。
先决条件
- 安装MySQL关系数据库管理系统 (DBMS)。
- 安装Go语言开发环境。有关安装说明,请参阅安装Go。
- 一个编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
- 一个命令行终端。Go在Linux和Mac中的任何终端,以及Windows中的PowerShell或cmd上都能很好地工作。
为你的代码创建一个文件夹
首先,为你要编写的代码创建一个文件夹。
1 打开命令提示符并切换到你的家目录。 在Linux或Mac上:
$ cd
在Windows上:
C:\> cd %HOMEPATH%
对于本教程的其余部分,我们将显示$作为提示符。我们使用的命令也适用于 Windows。
2 在命令提示符下,为你的代码创建一个名为data-access的目录。
$ mkdir data-access
$ cd data-access
3 创建一个模块,你可以在其中管理将在本教程中添加的依赖项。
运行go mod init
命令,为它提供新代码的模块路径。
$ go mod init example/data-access
go: creating new go.mod: module example/data-access
此命令创建一个go.mod文件,其中将列出你添加的依赖项以供追踪。有关更多信息,请参阅管理依赖项。
注意:在实际开发中,你应该指定一个更符合你自己需求的模块路径。有关更多信息,请参阅管理依赖项。
接下来,你将创建一个数据库。
建立一个数据库
在此步骤中,你将创建要使用的数据库。你将使用DBMS本身的CLI来创建数据库和表,以及添加数据。
你将创建一个数据库,其中包含有关黑胶唱片上的老式爵士乐唱片的数据。
此处的代码使用MySQL CLI,大多数DBMS都有自己的具有类似功能的CLI。
1 打开新的命令提示符。
2 在命令行中,登录DBMS,如下面的MySQL示例所示。
$ mysql -u root -p
Enter password:
mysql>
3 在mysql
命令提示符下,创建一个数据库。
mysql> create database recordings;
4 切换到你刚刚创建的数据库,以便你可以添加表。
mysql> use recordings;
Database changed
5 在文本编辑器的数据访问文件夹中,创建一个名为create-tables.sql的文件来保存用于添加表的SQL脚本。
6 在文件中,粘贴以下SQL代码,然后保存文件。
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代码中,你:
- 删除一个名为
album
的表。如果你想重新开始使用该表,执行此命令即可。 - 创建一个包含四列的
album
表:title
、artist
和price
。每行的id
值由DBMS自动创建。 - 添加四行值。
7 在mysql
命令提示符下,运行你刚刚创建的脚本。
使用以下形式的source
命令:
mysql> source /path/to/create-tables.sql
8 使用SELECT语句来验证你是否成功创建包含数据的表。
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页面以确定你可以使用的驱动程序。
使用页面上的列表来确定你将使用的驱动程序。为了在本教程中访问MySQL,你将使用Go-MySQL-Driver。
2 请注意驱动程序的包名称,此处为github.com/go-sql-driver/mysql
。
3 使用你的文本编辑器,创建一个用于编写你的Go代码的文件,命名为main.go
保存在你之前创建的数据访问目录中。
4 进入main.go
,粘贴以下代码导入驱动包。
package main
import "github.com/go-sql-driver/mysql"
在此代码中,你:
- 将你的代码添加到
main
包中,以便可以独立执行它。 - 导入MySQL驱动程序
github.com/go-sql-driver/mysql
。
导入驱动程序后,你将开始编写代码来访问数据库。
获取数据库句柄并连接
现在编写一些Go代码,使用数据库句柄(handler)访问数据库。
你将使用指向sql.DB
结构体的指针,该结构表示对特定数据库的访问。
编写代码
1 在main.go中,在你刚刚添加的import
代码下面,粘贴以下go代码以创建数据库句柄。
var db *sql.DB
func main() {
// 设置连接参数。
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// 获取一个数据库句柄。
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
作为全局变量简化了这个例子。在生产环境中,你应该避免使用全局变量,应该将变量传递给需要它的函数,或者将其封装在结构体中。
Config
结构体使得代码比直接使用连接字符串DSN更具可读性。
- 调用
sql.Open
初始化db
变量,并传入FormatDSN
函数的返回值。 - 检查
sql.Open
返回的错误变量。例如,如果数据库连接格式不正确,它可能会连接失败。
为了简化代码,我们调用log.Fatal
函数来结束执行并将错误打印到控制台。在生产环境中,你应该以更优雅的方式来处理错误。
- 调用
DB.Ping
函数以确认是否正常连接到数据库。在运行时,sql.Open
可能不会立即连接到数据库,具体取决于驱动程序。你在这里使用DB.Ping
函数来确认database/sql
包可以在需要时连接到数据库。 - 检查
Ping
函数是否返回错误,以防连接失败。 - 如果
Ping
函数连接成功,则打印消息。
2. 在main.go文件的顶部附近,就在包声明的下方,导入支持你刚刚编写的代码的包。
main.go文件的顶部现在应该是这样的:
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
模块作为你自己模块的一个依赖项。使用句点参数表示“获取当前目录中的代码的所有依赖项”。
$ go get .
go get: added github.com/go-sql-driver/mysql v1.6.0
Go下载了这个依赖项,因为你在上一步中将它添加到了import
声明中。有关跟踪依赖项的详细信息,请参阅添加一个依赖项。
在命令提示符下,设置DBUSER
和DBPASS
环境变量以供这个Go程序使用。
在Linux或Mac上:
$ export DBUSER=username
$ export DBPASS=password
在Windows上:
C:\Users\you\data-access> set DBUSER=username
C:\Users\you\data-access> set DBPASS=password
3. 在包含main.go的目录中的命令行中,通过键入go run
和一个点参数来运行代码,意思是“运行当前目录中的main包”:
$ go run .
Connected!
你可以连接成功!接下来,你将查询一些数据。
查询多行
在本节中,你将使用Go执行一个SQL查询,该查询旨在返回多行。
对于可能返回多行的SQL语句,可以使用database/sql
包中的Query
方法,然后循环获取它返回的行。(稍后将在查询单行一节中学习如何查询单行。)
编写代码
1. 进入main.go,在func main
的正上方,粘贴Album
结构体的以下定义。你将使用它来保存从查询返回的行数据。
type Album struct {
ID int64
Title string
Artist string
Price float32
}
2. 在func main
下,粘贴以下albumsByArtist
函数以查询数据库:
// albumsByArtist函数查询具有指定艺术家(Artist)姓名的专辑。
func albumsByArtist(name string) ([]Album, error) {
// 一个切片用于保存返回行数据的相册(Album)信息。
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("albumsByArtist %q: %v", name, err)
}
return albums, nil
}
在此代码中,你:
- 声明
Album
类型的一个album
切片,用来存储来自返回行的数据。结构体字段名称和类型对应于数据库列的名称和类型。 - 使用
DB.Query
执行SELECT语句以查询具有指定艺术家(Artist)姓名的专辑。
查询的第一个参数是SQL语句。在该参数之后,可以传递零个或多个任意类型的参数。这些参数的值将依次填入你在SQL语句中使用?
号占据的位置。通过将SQL语句与参数值分开(而不是将它们用fmt.Sprintf
函数连接成一个字符串),你可以让database/sql
包将参数值与SQL文本分开发送,从而消除任何SQL注入风险。
defer rows.Close()
延迟关闭rows
,以便在函数退出时释放它持有的任何资源。- 遍历返回的行,使用
Rows.Scan
将每行的列值分别分配给Album
结构体字段。
Scan
函数获取指向Go变量的指针列表,对应的列值将写入其中。在这里,你传入&
运算符加alb
结构体变量中的字段。通过这些指针Scan
函数写入以更新结构体的字段。
- 在循环体内,也检查
Scan
函数是否有错误返回。 - 在循环体内,将新的
alb
变量附加到albums
切片。 - 在循环体之后,使用
rows.Err
函数检查整个查询是否有错误发生。请注意,如果查询本身失败了,则检查此处的错误是发现结果不完整的唯一方法。
3. 更新你的main
函数以调用albumsByArtist
函数。
在func main
的末尾,添加以下代码:
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
在新代码中,你现在:
- 调用
albumsByArtist
函数,将其返回值分配给新的albums
变量。 - 打印结果。
运行代码
从包含main.go文件的目录的命令行运行代码:
$ 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
函数的代码:
// albumByID函数使用指定ID来查询相册(album)信息。
func albumByID(id int64) (Album, error) {
// 声明一个用来存储返回行数据的Album结构体变量。
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的相册的信息。
它返回一个sql.Row
。为了简化你的代码,QueryRow
函数不会返回错误。相反,它安排稍后的Rows.Scan
函数返回任何可能发生的查询错误(例如sql.ErrNoRows
)。
- 使用
Row.Scan
函数将列值复制到结构体字段中。 - 检查
Scan
函数返回错误。
特殊错误sql.ErrNoRows
表示查询未返回任何行。通常,该错误值得用更具体的文本来说明,例如此处的“没有这样的专辑(no such album
)”。
2. 更新main
函数以调用albumByID
函数。
在func main
的末尾,添加以下代码:
// 在此处硬编码ID为2以测试查询。
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
在这段代码中,你现在:
- 调用你添加的
albumByID
函数。 - 打印返回的相册的ID。
运行代码
在包含main.go的目录的命令行运行代码:
$ 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
语句以向数据库添加新行。
你已经了解了如何将Query
和QueryRow
函数与返回数据的SQL语句一起使用。如果要执行不返回数据的SQL语句,你可以使用Exec
函数。
写代码
1. 在albumByID
函数下方,粘贴以下addAlbum
函数以在数据库中插入新专辑的数据:
// 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
的末尾,添加以下代码:
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
的目录的命令行运行代码:
$ 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对关系数据库执行了简单的操作。
建议你进行下一个主题:
- 看看数据访问指南,其中包括更多关于此处仅涉及的主题的信息。
- 如果你是Go语言新手,你可以在Effective Go和How to write Go code中找到有用的最佳实践。
- Go Tour是对Go基础知识的一个很好的循序渐进的介绍。
完成全部代码
本节给出本教程的全部代码:
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() {
// 设置连接参数
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// 获取一个数据库句柄
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)
// 获取ID为2的相册的信息,测试查询语句
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函数查询具有指定艺术家(Artist)姓名的专辑。
func albumsByArtist(name string) ([]Album, error) {
// 一个切片用于保存返回行数据的相册(Album)信息。
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("albumsByArtist %q: %v", name, err)
}
return albums, nil
}
// albumByID函数使用指定ID来查询相册(album)信息。
func albumByID(id int64) (Album, error) {
// 声明一个用来存储返回行数据的Album结构体变量。
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函数添加指定专辑的数据到数据库,返回这条新记录的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
}