携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情
1.概述
要在 Go 中访问数据库,可以使用sql.DB。您可以使用此类型去创建语句和事务、执行查询和获取结果。
您应该知道的第一件事是sql.DB不是数据库连接。它也没有映射到任何特定的数据库软件中的「数据库」或「模式」的概念。它是一种存在于数据库中的接口的抽象,它可以像本地文件一样多种多样,可以通过网络连接访问,也可以在内存中和进程中访问。
通常 sql.DB 会在后台为您执行一些重要任务:
- 它通过驱动程序打开和关闭与实际底层数据库的连接。
- 它根据需要管理连接池,这可能是前面提到的各种各样的东西。
sql.DB 抽象旨在使您不必担心如何管理对底层数据存储的并发访问。当您使用连接来执行任务时,该连接会被标记为使用中,然后在不再使用时返回到可用池中。这样做的一个后果是,如果您无法将连接释放回池,则可能导致sql.DB 打开大量连接,这可能会耗尽资源(连接太多,打开文件句柄过多,缺少可用的网络端口等)。稍后我们将详细讨论。
创建 sql.DB 后,您可以使用它来查询它所表示的数据库,以及创建语句和事务。
2.导入数据库驱动
要使用 database/sql,您需要这个包本身,以及要使用的特定数据库的驱动程序。
通常情况下,您最好不要直接使用驱动包,尽管有些驱动提供者鼓励你这么做(在我们看来,这通常是个坏主意)。尽可能的引用 database/sql 包里定义的类型,这样做最大的好处是能够让你的代码与具体的驱动实现松耦合(或者完全解耦),这样直接更换底层驱动就可以切换不同数据库而不用担心驱动之间的差异,或者只需要很少的代码改动。其次,这样有助于你的代码更具有 Go 推荐的风格而不是某个驱动作者的代码风格。
在本文档中,我们将使用@julienschmidt 和 @arnehormann 提供的非常出色的 MySQL 驱动程序 (opens new window)作为示例。
将以下代码添加到您的 Go 源文件顶部:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
注意,我们使用_匿名加载 MySQL 驱动包。除了 init 函数执行外,go 将注册 MySQL 驱动以使其能够被 database/sql 包直接访问。
请注意,我们以匿名方式加载驱动程序,将其包限定符别名设置为 _,因此它导出的名称对我们的代码都不可见。在底层,驱动程序将自身注册为对 database/sql 包可用,但一般而言,除了运行 init 函数外,不会发生任何其他事情。
打开终端执行以下命令:
$ go get -u github.com/go-sql-driver/mysql
现在你可以开始访问数据库了。
3.连接数据库
加载驱动程序包后,就可以创建数据库对象了,一个sql.DB。
要创建sql.DB,请使用sql.Open()。这将返回一个*sql.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()
}
在所示的示例中,我们说明了几件事:
sql.Open的第一个参数是,驱动程序名称。这是驱动程序用来在database/sql中进行自身注册的字符串,通常与包名称相同,以避免混淆。例如,mysql用于 github.com/go-sql-driv…。某些驱动程序不遵循约定,而是使用数据库名称,例如,用于 github.com/mattn/go-sq…的sqlite3和用于 github.com/lib/pq (ope…的postgres。- 第二个参数是,特定于驱动程序的语法,它告诉驱动程序如何访问基础数据存储。在此示例中,我们将连接到本地 MySQL 服务器实例内的「hello」数据库。
- 您(几乎)应该始终检查并处理从所有
database/sql操作返回的错误。我们将在以后讨论一些特殊情况,以解决此问题。 - 如果
sql.DB的生存期不应超出该功能的范围,则推迟db.Close()是习惯的做法。
也许违反直觉,sql.Open() 不会建立与数据库的任何连接,也不会验证驱动程序的连接参数。而是它只准备数据库抽象以备后用。与基础数据存储区的第一个实际连接将延迟到第一次需要时建立。如果要立即检查数据库是否可用和可访问(例如,检查是否可以建立网络连接并登录),请使用 db.Ping() 进行操作,并记录检查错误:
err = db.Ping()
if err != nil {
// do something here
}
虽然在完成后对 Close() 数据库很习惯,但是 sql.DB 对象被设计为长期存在。不要经常使用 Open() 和 Close()。而是为每个需要访问的不同数据存储创建 一个 sql.DB 对象,并保留该对象直到程序完成对该数据存储的访问为止。根据需要传递它,或以某种方式使其在全局范围内可用,但保持打开状态。不要通过短期函数来 Open() 和 Close()。而是将 sql.DB 传递给该短期函数作为参数。
如果不将 sql.DB 视为长期对象,则可能会遇到诸如重用和连接共享不良,可用网络资源用尽或由于很多原因而导致的偶发性故障等问题 TIME_WAIT 状态中剩余的 TCP 连接数。这些问题表明您没有使用设计的 database/sql。
现在该使用您的 sql.DB 对象了。
4. 获取数据集
有几种常用的操作可以从数据库中检索结果。
- 执行返回一条数据的查询操作。
- 准备要重复使用的语句,在多次执行该语句后进行销毁。
- 一次性的执行语句,并且不打算将其重复使用。
- 执行返回一条数据的查询,这种特殊情况有一个快捷方式。
Go 的 database/sql 包中函数名称很重要。如果一个函数名字包含 Query, 那么该函数旨在向数据库发出查询问题, 并且即使它为空,也将返回一组行。不返回行的语句不应该使用 Query 函数; 而应使用 Exec()。
从数据库获取数据
让我们来看一个如何查询数据库和处理数据的例子. 我们将在用户表 users 中查询 id 为 1 的用户, 那么如何打印该用户的 id 和 name 呢,我们需要使用 rows.Scan() 函数把数据遍历并赋值给变量。
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
下面是此段代码所做的事情:
- 我们使用
db.Query()将查询请求发送给数据库,我们通常要检查是否报错。 - 我们需要在程序最后使用
rows.Close()来关闭,这是非常重要的。 - 我们将使用
rows.Next()来遍历每行的数据 - 我们使用
rows.Scan()来将每一行中每一列的数据赋值给变量。 - 当把每一行的数据进行遍历后,检查是否出错。
这几乎是 Go 中唯一的方法。例如,您无法获得一行作为 map。那是因为所有内容都是强类型的。您需要创建正确类型的变量,并将指针传递给它们。
其中的两个部分很容易出错,并可能带来严重的后果。
- 您应该始终检查
for rows.Next()循环的末尾是否有错误。如果循环过程中出现错误,您需要知道它。不要只假设循环会迭代,直到您处理完所有行。 - 其次,只要存在打开的结果集(由
rows表示),底层连接就很忙,不能用于任何其他查询。这意味着它在连接池中不可用。如果使用rows.Next()迭代所有行,最终将读取最后一行,并且rows.Next()将遇到内部 EOF 错误并为您调用rows.Close()。但是,如果出于某种原因您退出该循环——提前返回,等等情况——那么rows不会关闭,连接保持打开。(不过,如果rows.Next()由于错误返回 false,那么它会自动关闭)。这是耗尽资源的一种简单方式。 - 如果
rows.Close()已经关闭,那么它是一个无害的 no-op,因此您可以多次调用它。但是请注意,我们首先检查错误,只有在没有错误时才调用rows.Close(),以避免 runtime panic。 - 您应该 始终
defer rows.Close(),即使您也在循环末尾显式调用rows.Close(),这不是个坏主意。 - 不要在循环内
defer。在函数退出之前,不会执行延迟语句,因此长时间运行的函数不应使用该语句。如果这样做,您将慢慢积累内存。如果要在循环中重复查询和使用结果集,则应在处理完每个结果后显式调用rows.Close(),而不要使用defer。
Scan() 如何工作
当您遍历行并将其扫描到目标变量中时,Go 会在后台执行数据类型转换。它基于目标变量的类型。意识到这一点可以清理您的代码并有助于避免重复的工作。
例如,假设您从用字符串列定义的表中选择一些行,例如VARCHAR(45)或类似名称。但是,您偶然知道该表始终包含数字。如果将指针传递给字符串,Go 会将字节复制到字符串中。现在您可以使用 strconv.ParseInt() 或类似方法将值转换为数字。您必须检查 SQL 操作中的错误以及解析整数的错误。这是混乱而又乏味的。
或者,您可以仅传递 Scan() 指向整数的指针。 Go 将检测到该情况并为您调用 strconv.ParseInt()。如果转换中出现错误,则对 Scan() 的调用将返回该错误。您的代码现在变得更整洁,更小了。这是使用 database/sql 的推荐方法。
预处理查询
通常,您应该始终对要多次使用的查询执行预处理。预处理查询的结果是一个预处理语句,该语句可以具有占位符(也称为绑定值),用于执行该语句时将提供的参数。出于所有常见原因(例如,避免 SQL 注入攻击),这比串联字符串好得多。
在 MySQL 中,参数占位符是 ?,而在 PostgreSQL 中则是 $N,其中 N 是一个数字。 SQLite 接受任何一种。在 Oracle 中,占位符以冒号开头并被命名,例如 :param1。我们将使用 ?,因为我们使用 MySQL 作为例。
stmt,err := db.Prepare("select id from users where id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next(){
// ...
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
在幕后,db.Query() 实际上做了预处理、执行并关闭了预处理语句。这是到数据库的三次往返。如果不小心,您的应用程序所进行的数据库交互次数可能会增加三倍!某些驱动程序在特定情况下可以避免这种情况,但并非所有驱动程序都可以。有关详细信息,请参阅 预处理语句 (opens new window)。
单条记录查询
查询结果多于一行的时候,这样来获取单条记录:
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
错误信息会在 Scan() 之后返回。 QueryRow() 也可以用于预处理语句:
stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)