Go语言数据库驱动程序基础构建指南(五)
Fetching Result Sets
database/sql库提供了一些方法用于查询并返回结果集:db.Query()和db.QueryRow()。我们已经看到了前者的一个例子,在本节中将介绍后者。
如前所述,使用SQL查询执行db.Query()将执行以下操作:
- 1.从连接池获取一个连接
- 2.执行查询操作
- 3.将连接的所有权转移给结果集
结果集是一个传统上称为行(rows)的sql.Rows变量,如果不需要更多的描述性名称,那么它就是结果上的光标。从第一行开始,调用rows.Next()可以用来获取行的内容。光标的初始位置在第一行之前。
重复前面的例子:
rows, err := db.Query("SELECT * FROM test.hello") if err != nil {
log.Fatal(err) }
for rows.Next() { var s string
err = rows.Scan(&s) if err != nil {
log.Fatal(err) }
log.Printf("found row containing %q", s)
} rows.Close()
关于这段代码有几点需要注意,让我们呢从外到内分析下,先看看用来迭代行的rows.Next()。
Iterating Over Rows In A Result(在结果中迭代行)
rows.Next()函数设计用于for循环,就像上边代码里那样。遇到错误时,包括表示已到达行的结尾而发出的io.EOF信号,它将返回false。 在正常操作中,你通常会迭代所有直到最后一行,然后退出循环。
但是,如果你不正常退出循环怎么办?如果故意break循环或直接从函数return怎么办呢?如果你这么干了,你的的结果将不会被完全提取和处理,并且连接可能不会被释放回池中。正确处理行需要考虑这种可能性。你的目标是一定要调用** rows.Close()**将连接释放给连接池。如果没有的话,这个连接将永远不会被返回给连接池,这样的话会导致严重问题“连接泄露”。如果你不注意就很容易出现这个问题导致服务器出错或者达到服务器最大连接数,让系统宕机。
如何防止出现这样的情况呢?第一,你肯定很乐于了解到如果循环是由rows.Next()返回false时结束的,无论正常还是异常,rows.Close()都会自动调用。所以在正常操作下,这些情况里你不会保留未返回连接池的连接。
需要注意的情况时提前返回或者Break中断循环时,在这些情况下你应该做些什么需要视具体情况而定。如果你在处理结束时在循环内返回,你应该使用defer rows.Close()。这是确保“必须运行”代码确实始终在函数返回时运行的惯用方法。并且在创建资源之后立即进行这样的清理调用也是惯用的(这对程序的正确性很重要)。我们改良的代码长这样:
rows, err := db.Query("SELECT * FROM test.hello")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
但是,如果封闭函数是长活的并且你会在循环中反复查询,那么你不应该推迟关闭行。你应该在断开循环之前明确地做到这一点。事实上,有个通用规则是你应该尽可能早的调用rows.Close()去释放资源。在一些复杂的情况下,这需要对代码进行一些思考和分析。
这里有两条关于在长活程序中不推迟关闭的理由:
- 1.在长活函数里延迟执行的代码可能很久都不会执行,但是你却需要它尽快执行以清理资源
- 2.延迟函数的变量占着内存,万一它真的很久很久都不被调用,这里就内存泄露了。
通过我们后面的结果集清理,让我们看看结果集循环异常退出的处理情况。我们已经看到它退出的正常原因是当循环遇到io.EOF错误时,使rows.Next()返回false。任何时候rows.Next()找到一个error,都会把它在内部保存起来,并且在下一次检查时返回以推出循环。你可以在rows.Err()中查看。我们上面的代码没有体现对error的处理,不过在生产代码中,你应该在循环结束后检查错误,像这段代码一样:
for rows.Next() {
// process the rows
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
io.EOF错误在rows.Err()内当作特殊情况处理了。你不需要在代码里处理这样的情况,发生这样的情况时rows.Err只会给你返回nil。
除了一个小细节之外,上述几乎总结了你需要知道的关于循环行的所有内容:处理来自rows.Close()的错误。有趣的是,这个函数确实会返回一个错误,但这是一个很好的问题,可以用它做什么呢?如果你的代码没有理由去处理它(我们还没有看到它的情况),那么你可以随意忽略它或只是记录并继续执行。
Fetching A Single Row(获取单行)
获取单行是一项非常常见的任务,但使用前面显示的代码却很尴尬。你必须写一个循环,检查循环实际上有哪些行等等操作。幸运的是,有db.QueryRow()可以为你做这个。它执行一个预期返回零或一行的查询,并返回一个可扫描的sql.Row对象。通常的习惯用法是链接查询并一起扫描,如下所示:
var s string
err = db.QueryRow("select * from hello.world limit 1").Scan(&s) if err != nil {
if err == sql.ErrNoRows {
// special case: there was no row
} else {
log.Fatal(err) }
}
log.Println("found a row", s)
如你所见,写法和之前的略有不同。在内部,sql.Row对象包含查询中的错误或查询中的sql.Rows。如果发生错误,那么.Scan()方法会返回延迟错误。如果没有错误,.Scan()方法就可以正常工作,除非没有查询结果,那时它会返回一个特殊的误差常数,sql.ErrNoRows。你可以检查此错误以确定对.Scan()的调用是否实际执行并将该行中的值复制到目标变量中。
How rows.Scan() Works
使用rows.Scan()和它的单行变量没有想象中那么简单。在这个函数名背后,它替你做了许多事情。了解这些背后的事情,可以帮你在使用时更加得心应手。
rows.Scan()函数的参数是存储这一行数据的变量。通常这些将是变量的直接指针,使用&运算符引用,就像这样:
var var1, var2 string
err = rows.Scan(&var1, &var2)
参数类型是空接口interface{},Go语言中的任何类型都可以。在大多数情况下,Go会将行中的数据复制到你提供的目的地。这里是特殊情况,如果你愿意,可以避免复制,但你必须使用* sql.RawBytes类型来做到这一点,并且内存属于
数据库,有效期有限。如果你想这么干,请先阅读这个文档和源码以学习它的工作原理,大多数不这么干的人没这个必要。通常情况下,你会得到一份数据副本,可以随意使用。注意,由于database/sql的内部限制,不可以将*sql.RawBytes和db.QueryRow().Scan()一起使用。
database/sql包会检查目标参数的类型,大多数情况下会转换值。这有助于减少你的代码量,尤其是错误处理的代码。举个例子,假设你有一个字段是数字类型,但由于一些原因它不是数字,而是VARCHAR在ASCII格式的数字。我们可以用字符串变量接收,然后把它转成数字,自己检查每一步可能的错误。但是我们不必如此,因为database/sql会帮我们搞定。如果我们将一个float64目标变量传递给调用,Scan()将检测到我们正在尝试将字符串扫描到一个数字中,为我们调用strconv.ParseFloat()并返回可能出现的错误。
查询的一个特殊情况是数据库中的值为NULL。NULL值无法被复制到普通变量中,而且你也不能传递nil参数到rows.Scan()函数。相反,你必须使用特殊类型作为扫描目标。这些类型在database/sql中为许多常见类型定义,例如sql.NullFloat64等等。如果你需要一个未定义过的类型,你可以先看看你的驱动程序有没有提供,或者复制/粘贴源代码自己创建一个;也就三两行代码而已。查询结束之后,你可以检查你的数据是否有效,如果有效你就可以用了。(如果你不在乎,你可以跳过有效性检查; 读取该值将给出基础类型的零值。)
总而言之,对rows.Scan()的更复杂调用可能如下所示:
var (
s1 string
s2 sql.NullString i1 int
f1 float64
f2 float64
)
// Suppose the row contains ["hello", NULL, 12345, "12345.6789", "not-a-float"]
err = rows.Scan(&s1, &s2, &i1, &f1, &f2) if err != nil {
log.Fatal(err) }
调用rows.Scan()可能会导致下面的报错,说明最后一列自动转换为浮点数,但失败了:
sql: Scan error on column index 4: converting string "not-a-float" to a float64: strconv.ParseFloat: parsing "not-a-float": invalid syntax
但是,由于参数按顺序处理,其余的扫描都会成功,通过删除对log.Fatal()的调用,我们可以看到以下代码行:
err = rows.Scan(&s1, &s2, &i1, &f1, &f2)
log.Printf("%q %#v %d %f %f", s1, s2, i1, f1, f2)
程序输出是:
"hello" sql.NullString{String:"", Valid:false} 12345 12345.678900
0.000000
这说明s2变量的Valid字段为false,其String字段为空,如预期的那样。你可以检查此变量并根据需要处理该变量:
if s2.Valid {
// use s2.String
}
What If You Don’t Know The Columns? (如果不知道列的信息怎么办?)
有时你会查询可能返回未知数量的具有未知名称和类型的列的内容。 例如,假设你在备份程序中执行SELECT *。 或者你可能在服务器的不同版本中查询具有不同列的内容,例如MySQL中的SHOW FULL PROCESSLIST
。
database/sql提供了一个方法获取字段名,也包括字段数,但是不提供字段类型。想拿到这样字段名,可以使用rows.Columns,这个方法可能会返回错误,最好先检查:
cols, err := rows.Columns()
if err != nil {
log.Fatal(err)
}
现在你可以用结果集做一些有用的事情了。最简单的,当你知道字段名和类型时并且想在不同场景下得到可变数量的变量时,你可以写下面这样的代码。这表示你期望最多获得5个字段值但通常情况是比5个少的,字段类型分别是uint64, string, string, string, uint32。定义一个包含非空变量的interface{}切片处理最多的情况,然后把这个切片作为Scan()的参数。
dest := []interface{}{
new(uint64),
new(string),
new(string),
new(string),
new(uint32),
}
err = rows.Scan(dest[:len(cols)])
如果你不知道字段名或者数据类型,你需要借助sql.RawBytes。
cols, err := rows.Columns()
vals := make([]interface{}, len(cols))
for i, _ := range cols {
vals[i] = new(sql.RawBytes)
}
for rows.Next() {
err = rows.Scan(vals...)
}
查询之后,你可以检查vals切片,遍历每个元素是否为nil,并使用类型自省和类型断言来计算变量的类型并处理它。结果代码通常不是很漂亮,但在处理未知数据时,这是你最好的做法。
Working With Multiple Result Sets And Multiple Statements(使用多个结果集和多个语句)
database/sql并没有提供返回多个结果集的函数。不和谐的说一句。这很大程度上取决于数据库和驱动程序实现,但是database/sql本身是为查询返回单个结果集而设计的,并且在获取第一行后无法获取下一个结果集或处理列中的更改。
至少在MySQL中,这也使调用存储过程变得尴尬,即使是那些不返回多个结果集的存储过程也是如此。原因是即使MySQL的网络协议进入这种情况的多语句模式,也只返回一个结果集,如果你执行CALL XXX.YYY语句的话就会得到以下报错:
Error 1312: PROCEDURE XXX.YYY can’t return a result set in the
given context.
类似地,一些数据库允许在单个查询中发送多个语句,可能以分号分隔等。但是database/sql不会为它构建,因此结果是未定义的行为。举例说,以下内容可能会执行一个或两个语句,或者只是抛出一个错误,具体取决于驱动程序和数据库。
_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2")