教程:访问关系型数据库
本教程介绍了使用 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 代码中,您 - 
删除(删除)一个名为 album的表。首先执行此命令使您以后更容易重新运行脚本,如果您想重新开始使用该表。
- 
创建一个包含四列的 album表:id、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。
- 
使用文本编辑器,创建一个文件来编写您的 Go 代码,并将其保存为 main.go 在您之前创建的 data-access 目录中。 
- 
将以下代码粘贴到 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() { // Capture connection properties. 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" // 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 驱动程序的 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.1Go 下载了这个依赖项,因为您在上一步中将其添加到了 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
- 
在包含 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 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 }在此代码中,您 - 
声明一个您定义的 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 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 }在此代码中,您 - 
使用 DB.QueryRow执行SELECT语句以查询具有指定 ID 的专辑。它返回一个 sql.Row。为了简化调用代码(您的代码!),QueryRow不会返回错误。相反,它会安排稍后从Rows.Scan返回任何查询错误(例如sql.ErrNoRows)。
- 
使用 Row.Scan将列值复制到结构体字段中。
- 
检查 Scan返回的错误。特殊错误 sql.ErrNoRows表示查询没有返回任何行。通常,这个错误值得用更具体的文本替换,例如这里的“没有这样的专辑”。
 
- 
- 
更新 main以调用albumByID。在 func main的末尾,添加以下代码。// 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)在新代码中,您现在 - 
调用您添加的 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 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 }在此代码中,您 - 
使用 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() {
    // Capture connection properties.
    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"
    // 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
}