在 Go 中使用 SQL 数据库的惯用方法是使用 database/sql 包。它为面向行的数据库提供了一个轻量级的接口。本教程会告诉你 database/sql 的常见用法。

为什么需要本教程?该包的文档会告诉你每一个 API 的功能,但并没有告诉你如何使用该包。我们中的许多人发现自己希望有一个快速入门教程,这个教程应该“讲故事”而不是“列事实”。如果你想对本教程做出贡献,欢迎提交 PR

概览

要在 Go 中访问数据库,你可以使用 sql.DB 来创建语句和事务、执行查询,并获取结果。

但注意,sql.DB 并不是一个数据库连接。它也没有映射到数据库软件的 database 或 scheme。它只是对数据库接口和数据库本身的抽象,它可能是一个通过网络连接访问的本地文件,也可能是某进程中的一块内存。

sql.DB 在背后为你执行了以下重要的任务:

一旦使用了 sql.DB,你就不用为数据库的并发访问担心了。当你使用一个连接(connection)时,该连接会被标记为使用中;当你不再使用这个连接时,它就会被返回到连接池中。但这样做有一个后果:如果连接没有被成功地释放回池子里,就会导致 sql.DB 打开很多连接,这很有可能会耗尽资源(太多的连接,太多的打开的文件句柄,缺乏可用的网络端口等等)。后面我们会继续讨论这个问题。

在创建一个 sql.DB 之后,你可以用它来查询数据库、创建语句和事务。

导入数据库驱动

在使用 database/sql 时,你不仅需要导入对应的包(package),你还需要导入特定的数据库驱动。

一般我们不应该直接使用驱动包,不过依然有些驱动包鼓励你这么做(我们不推荐这么做)。你的代码应该只引用 database/sql 中定义的类型。这有助于让你的代码与数据驱动解耦。另外,这种做法能避免你受到该驱动包作者的代码风格的影响,写出更符合 Go 社区偏好的代码。

在本教程中,我们将使用 @julienschmidt 和 @arnehormann 的 MySQL 驱动作为例子,这个驱动非常优秀。

在你的 Go 文件顶部添加以下内容:

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)

可以看到,我们以匿名加载的方式引入了 mysql 驱动程序,这样我们的代码就访问不到它导出的类型和变量了。实际上驱动程序将被注册为 database/sql 的驱动。一般来说,驱动程序只会运行 init 函数。

现在你可以访问数据库了。

访问数据库

我们已经加载了驱动包,现在可以创建一个数据库对象,即 sql.DB。

使用 sql.Open() 得到 sql.DB 的指针(即 db 变量):

func main() {
	db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}

关于上面的例子,有以下几点说明:

  1. sql.Open 的第一个参数是驱动程序的名称。该名称用于注册 database/sql 的驱动,习惯上与包名相同。例如,github.com/go-sql-driver/mysql 的注册名称是 mysql。但,一些驱动程序不遵循惯例,例如,github.com/mattn/go-sqlite3 的注册名称是 sqlite3,而 github.com/lib/pq 的注册名称是 postgres。
  2. sql.Open 的第二个参数是连接字符串,它告诉驱动如何访问数据。本例中的连接字符串表示我们要连接到本地 MySQL 服务器中的 "hello" 数据库。
  3. 如未作特殊说明,你应该总是检查和处理 database/sql 操作返回的错误。
  4. 如果你希望 sql.DB 在当前函数结束之后关闭,那么 defer db.Close() 是一个好习惯。

你可能以为 sql.Open() 会连接数据库,但实际上并没有,它也没有验证连接字符串。它只是为后面使用数据库做好了准备。数据库的连接只会在你真正对数据库进行操作时创建。如果你想知道数据库是否可以成功连接(网络连接是否正常,用户名密码是否正确),那么你可以使用 db.Ping(),同时不要忘了检查错误:

err = db.Ping()
if err != nil {
	// do something here
}

虽然在完成数据库操作后调用 Close() 方法是惯用法,但 sql.DB 是被设计成长时间使用的。不要频繁地 Open() 和 Close() 数据库,而是为需要访问的每个不同数据库创建一个 sql.DB 对象,并将其保留到程序完成访问该数据库为止。你应该保持 sql.DB 处于 Open 状态,然后把 sql.DB 传给需要访问数据的模块,或者你也可以让 sql.DB 全局可用。不要在一个局部函数中调用 Open() 和 Close() 方法。而是把 sql.DB 作为参数传递给它。

如果你不把 sql.DB 当作一个长时间使用的对象,那么你可能会遇到一些问题,比如连接的复用和共享效率低、网络资源被耗尽,或者由于大量的 TCP 连接停留在 TIME_WAIT 状态而出现间歇性故障。这些问题说明你没有按照 database/sql 库的设计来使用它。

现在,我们可以使用 sql.DB 对象了。