教程:访问关系型数据库

本教程介绍使用 Go 及其标准库中的 database/sql 包访问关系型数据库的基础知识。

如果您对 Go 及其工具链有基本了解,将能更好地利用本教程。如果您是第一次接触 Go,请参阅教程:开始使用 Go 进行快速入门。

您将使用的 database/sql 包包含用于连接数据库、执行事务、取消正在进行的操作等的类型和函数。有关使用此包的更多详细信息,请参阅访问数据库

在本教程中,您将创建一个数据库,然后编写代码来访问该数据库。您的示例项目将是一个关于老式爵士乐唱片的数据存储库。

在本教程中,您将按以下章节进行学习:

  1. 为您的代码创建一个文件夹。
  2. 设置数据库。
  3. 导入数据库驱动程序。
  4. 获取数据库句柄并连接。
  5. 查询多行数据。
  6. 查询单行数据。
  7. 添加数据。

注意:有关其他教程,请参阅教程

前提条件

为您的代码创建一个文件夹

首先,为您的代码创建一个文件夹。

  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. 在您的文本编辑器中,在 data-access 文件夹中,创建一个名为 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 代码中,您将执行以下操作:

    • 删除 (drop) 一个名为 album 的表。先执行此命令可以方便您以后重新运行脚本,如果想从头开始的话。

    • 创建一个名为 album 的表,包含四列:titleartistprice。每行的 id 值由 DBMS 自动创建。

    • 添加四行带有值的数据。

  7. mysql 命令行提示符下,运行您刚创建的脚本。

    您将使用以下形式的 source 命令:

    mysql> source /path/to/create-tables.sql
    
  8. 在您的 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 包中的函数发出的请求转换为数据库能理解的请求。

  1. 在您的浏览器中,访问 SQLDrivers wiki 页面,以确定可以使用哪个驱动程序。

    使用页面上的列表确定您将使用的驱动程序。在本教程中,要访问 MySQL,您将使用 Go-MySQL-Driver

  2. 请注意驱动程序的包名——此处为 github.com/go-sql-driver/mysql

  3. 使用您的文本编辑器,创建一个文件来编写您的 Go 代码,并将文件保存为 main.go 在您之前创建的 data-access 目录中。

  4. 将以下代码粘贴到 main.go 中,以导入驱动程序包。

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

    在此代码中,您将执行以下操作:

    • 将您的代码添加到 main 包中,以便可以独立执行它。

    • 导入 MySQL 驱动程序 github.com/go-sql-driver/mysql

导入驱动程序后,您将开始编写代码以访问数据库。

获取数据库句柄并连接

现在编写一些 Go 代码,使用数据库句柄让您可以访问数据库。

您将使用指向 sql.DB 结构体的指针,它表示对特定数据库的访问。

编写代码

  1. 在 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.DBdb 变量。这就是您的数据库句柄。

      db 声明为全局变量简化了本示例。在生产环境中,您应该避免使用全局变量,例如通过将其作为参数传递给需要它的函数,或将其封装在结构体中。

    • 使用 MySQL 驱动程序的 Config——以及该类型的 FormatDSN——来收集连接属性并将它们格式化为 DSN 作为连接字符串。

      使用 Config 结构体可以使代码比使用连接字符串更容易阅读。

    • 调用 sql.Open 初始化 db 变量,传入 FormatDSN 的返回值。

    • 检查 sql.Open 返回的错误。如果数据库连接细节格式不正确,可能会失败。

      为了简化代码,您调用 log.Fatal 来终止执行并将错误打印到控制台。在生产代码中,您会希望以更优雅的方式处理错误。

    • 调用 DB.Ping 确认连接数据库是否工作正常。在运行时,sql.Open 可能不会立即连接,具体取决于驱动程序。您在这里使用 Ping 来确认 database/sql 包在需要时可以连接。

    • 检查 Ping 返回的错误,以防连接失败。

    • 如果 Ping 成功连接,则打印一条消息。

  2. 在 main.go 文件顶部附近,紧接在 package 声明下方,导入支持您刚编写的代码所需的包。

    文件顶部现在应该如下所示:

    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: added filippo.io/edwards25519 v1.1.0
    go: added github.com/go-sql-driver/mysql v1.8.1
    

    由于您在上一步中将其添加到了 import 声明中,Go 下载了这个依赖项。有关依赖项跟踪的更多信息,请参阅添加依赖项

  2. 在命令行提示符下,为 Go 程序设置 DBUSERDBPASS 环境变量。

    在 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 并使用点号参数,表示“运行当前目录中的包”。

    $ 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 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 检查整个查询是否发生错误。请注意,如果查询本身失败,这里检查错误是唯一发现结果不完整的方法。

  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 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 表示查询没有返回任何行。通常,这个错误值得替换为更具体的文本,例如这里的“没有这样的专辑”。

  2. 更新 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 语句,向数据库中添加新行。

您已经了解了如何将 QueryQueryRow 与返回数据的 SQL 语句一起使用。要执行不返回数据的 SQL 语句,您可以使用 Exec

编写代码

  1. 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 时是否发生错误。

  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 对关系型数据库执行了简单的操作。

建议的下一主题

完成的代码

本节包含您通过本教程构建的应用程序的代码。

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
}