访问关系型数据库
本教程介绍了如何使用 Go 和标准库中的 database/sql 包访问关系型数据库的基础知识。
如果你对 Go 及其工具链有基本的了解,你将能从本教程中获得最大收益。如果你是第一次接触 Go,请参阅 教程:Go 入门 以快速了解。
你将使用的 database/sql 包包含了用于连接数据库、执行事务、取消正在进行的操作等的类型和函数。有关使用该包的更多详细信息,请参阅 访问数据库。
在本教程中,你将创建一个数据库,然后编写代码来访问该数据库。你的示例项目将是一个关于复古爵士唱片的数据存储库。
在本教程中,你将经历以下部分:
- 为代码创建一个文件夹。
- 设置数据库。
- 导入数据库驱动。
- 获取数据库句柄并连接。
- 查询多行。
- 查询单行。
- 添加数据。
先决条件
- MySQL 关系型数据库管理系统 (DBMS) 的安装。
- Go 的安装。 有关安装说明,请参阅 安装 Go。
- 一个用于编辑代码的工具。 任何你拥有的文本编辑器都可以正常工作。
- 一个命令终端。 Go 在 Linux 和 Mac 上的任何终端上都能很好地工作,在 Windows 上的 PowerShell 或 cmd 上也是如此。
为代码创建一个文件夹
首先,为要编写的代码创建一个文件夹。
-
打开命令提示符并切换到你的主目录。
在 Linux 或 Mac 上:
$ cd在 Windows 上:
C:\> cd %HOMEPATH%在本教程的其余部分,我们将显示
$作为提示符。我们使用的命令在 Windows 上也能工作。 -
从命令提示符,创建一个名为
data-access的代码目录。$ mkdir data-access $ cd data-access -
在此教程中,你将管理要添加的依赖项,因此创建一个模块。
运行
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,功能相似。
-
打开一个新的命令提示符。
-
在命令行上,登录到你的 DBMS,如以下 MySQL 示例所示。
$ mysql -u root -p Enter password: mysql> -
在
mysql命令提示符下,创建一个数据库。mysql> create database recordings; -
切换到刚刚创建的数据库,以便你可以添加表。
mysql> use recordings; Database changed -
在你的文本编辑器中,在
data-access文件夹中,创建一个名为create-tables.sql的文件,用于保存添加表的 SQL 脚本。 -
将以下 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 代码中,你:
-
删除(drop)一个名为
album的表。首先执行此命令,可以让你在以后想要重新开始表时更容易重新运行脚本。 -
创建一个包含四列的
album表:title、artist和price。每行的id值由 DBMS 自动创建。 -
添加四行值。
-
-
从
mysql命令提示符,运行你刚刚创建的脚本。你将使用以下形式的
source命令:mysql> source /path/to/create-tables.sql -
在你的 DBMS 命令提示符下,使用
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 包中通过函数发出的请求翻译成数据库能理解的请求。
-
在你的浏览器中,访问 SQLDrivers wiki 页面,以确定你可以使用的驱动。
使用该页面上的列表来确定你将使用的驱动。在本教程中,为了访问 MySQL,你将使用 Go-MySQL-Driver。
-
记下驱动的包名 —— 在这里是
github.com/go-sql-driver/mysql。 -
使用你的文本编辑器,在之前创建的
data-access目录中创建一个用于编写 Go 代码的文件,并将其保存为main.go。 -
将以下代码粘贴到
main.go中,以导入驱动包。package main import "github.com/go-sql-driver/mysql"在这段代码中,你:
-
将你的代码添加到
main包中,以便你可以独立执行它。 -
导入 MySQL 驱动
github.com/go-sql-driver/mysql。
-
导入了驱动后,你将开始编写访问数据库的代码。
获取数据库句柄并连接
现在编写一些 Go 代码,让你通过数据库句柄访问数据库。
你将使用指向 sql.DB 结构的指针,该结构表示对特定数据库的访问。
编写代码
-
在
main.go中,紧接在你刚添加的import代码下方,粘贴以下 Go 代码以创建数据库句柄。var db *sql.DB func main() { // 捕获连接属性。 cfg := mysql.NewConfig() cfg.User = os.Getenv("DBUSER") cfg.Passwd = os.Getenv("DBPASS") cfg.Net = "tcp" cfg.Addr = "127.0.0.1:3306" cfg.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设为全局变量简化了这个示例。在生产环境中,你应避免使用全局变量,例如通过将变量传递给需要它的函数或将其包装在结构体中。 -
使用 MySQL 驱动的
Config结构体及其FormatDSN方法来收集连接属性,并将其格式化为连接字符串的 DSN。Config结构体使代码比连接字符串更易读。 -
调用
sql.Open初始化db变量,传递FormatDSN的返回值。 -
检查
sql.Open的错误。如果数据库连接细节格式不正确,它可能会失败。为简化代码,你调用
log.Fatal来终止执行并将错误打印到控制台。在生产代码中,你应以更优雅的方式处理错误。 -
调用
DB.Ping确认连接数据库是否有效。运行时,sql.Open可能不会立即连接,具体取决于驱动。你在此使用Ping来确认database/sql包在需要时可以连接。 -
检查
Ping的错误,以防连接失败。 -
如果
Ping成功连接,则打印一条消息。
-
-
在
main.go文件的顶部,紧接在包声明下方,导入你刚编写代码所需的包。文件顶部现在应如下所示:
package main import ( "database/sql" "fmt" "log" "os" "github.com/go-sql-driver/mysql" ) -
保存
main.go。
运行代码
-
开始将 MySQL 驱动模块作为依赖项进行追踪。
使用
go get将github.com/go-sql-driver/mysql模块添加为你自己模块的依赖项。使用点参数表示“获取当前目录中代码的依赖项”。$ go get . go: added filippo.io/edwards25519 v1.1.0 go: added github.com/go-sql-driver/mysql v1.8.1因为你将驱动添加到了之前的
import声明中,Go 下载了这个依赖项。有关依赖项追踪的更多信息,请参阅 添加依赖项。 -
从命令提示符,设置
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 -
在包含
main.go的目录的命令行中,通过键入go run并使用点参数(表示“运行当前目录中的包”)来运行代码。$ go run . Connected!
你已成功连接!接下来,你将查询一些数据。
查询多行
在本节中,你将使用 Go 执行旨在返回多行的 SQL 查询。
对于可能返回多行的 SQL 语句,你使用 database/sql 包中的 Query 方法,然后循环遍历它返回的行。(你将在后面的 查询单行 部分学习如何查询单行。)
编写代码
-
在
main.go中,紧接在func main上方,粘贴以下Album结构体定义。你将用它来保存查询返回的行数据。type Album struct { ID int64 Title string Artist string Price float32 } -
在
func main下方,粘贴以下albumsByArtist函数以查询数据库。// albumsByArtist 查询具有指定艺术家名称的专辑。 func albumsByArtist(name string) ([]Album, error) { // 一个 albums 切片,用于保存返回行中的数据。 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类型的albums切片。这将保存返回行中的数据。结构体字段名称和类型对应数据库列名称和类型。 -
使用
DB.Query执行SELECT语句,查询具有指定艺术家名称的专辑。Query的第一个参数是 SQL 语句。在参数之后,你可以传递零个或多个任何类型的参数。这些参数为你在 SQL 语句中指定的参数值提供位置。通过将 SQL 语句与参数值分开(而不是用fmt.Sprintf拼接),你使database/sql包能够将值与 SQL 文本分开发送,消除 SQL 注入风险。 -
延迟关闭
rows,以便在函数退出时释放它持有的任何资源。 -
循环遍历返回的行,使用
Rows.Scan将每行的列值分配给Album结构体字段。Scan接受一个指向 Go 值的指针列表,列值将写入这些值。在这里,你传递使用&运算符创建的alb变量中的字段指针。Scan通过这些指针写入以更新结构体字段。 -
在循环内,检查将列值扫描到结构体字段时发生的错误。
-
在循环内,将新的
alb追加到albums切片。 -
在循环后,使用
rows.Err检查整体查询的错误。注意,如果查询本身失败,检查此处的错误是唯一能发现结果不完整的方法。
-
-
更新你的
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 循环更简单。
编写代码
-
在
albumsByArtist下方,粘贴以下albumByID函数。// albumByID 查询具有指定 ID 的专辑。 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”。
-
-
更新
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。
编写代码
-
在
albumByID下方,粘贴以下addAlbum函数以在数据库中插入新专辑,然后保存main.go。// 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 的错误。
-
-
更新
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 和 如何编写 Go 代码 中发现有用的最佳实践。
-
Go 教程 是 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.NewConfig()
cfg.User = os.Getenv("DBUSER")
cfg.Passwd = os.Getenv("DBPASS")
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.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 查询具有指定艺术家名称的专辑。
func albumsByArtist(name string) ([]Album, error) {
// 一个 albums 切片,用于保存返回行中的数据。
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 的专辑。
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
}