• 简体中文
  • 访问关系型数据库

    本教程介绍了如何使用 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
    }