Go语言基地

访问关系型数据库

本教程介绍了如何使用 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. 在你的文本编辑器中,在 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. 使用你的文本编辑器,在之前创建的 data-access 目录中创建一个用于编写 Go 代码的文件,并将其保存为 main.go

  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() {
    	// 捕获连接属性。
    	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.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 main
    
    import (
    	"database/sql"
    	"fmt"
    	"log"
    	"os"
    
    	"github.com/go-sql-driver/mysql"
    )
  3. 保存 main.go

运行代码

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

    使用 go getgithub.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. 从命令提示符,设置 DBUSERDBPASS 环境变量,供 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 并使用点参数(表示“运行当前目录中的包”)来运行代码。

    $ 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 查询具有指定艺术家名称的专辑。
    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 检查整体查询的错误。注意,如果查询本身失败,检查此处的错误是唯一能发现结果不完整的方法。

  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 的专辑。
    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 语句,向数据库添加新行。

你已经了解了如何使用 QueryQueryRow 执行返回数据的 SQL 语句。要执行 返回数据的 SQL 语句,你使用 Exec

编写代码

  1. 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 的错误。

  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如何编写 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
}

On this page