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.RawBytesdb.QueryRow().Scan()一起使用。

database/sql包会检查目标参数的类型,大多数情况下会转换值。这有助于减少你的代码量,尤其是错误处理的代码。举个例子,假设你有一个字段是数字类型,但由于一些原因它不是数字,而是VARCHAR在ASCII格式的数字。我们可以用字符串变量接收,然后把它转成数字,自己检查每一步可能的错误。但是我们不必如此,因为database/sql会帮我们搞定。如果我们将一个float64目标变量传递给调用,Scan()将检测到我们正在尝试将字符串扫描到一个数字中,为我们调用strconv.ParseFloat()并返回可能出现的错误。

查询的一个特殊情况是数据库中的值为NULLNULL值无法被复制到普通变量中,而且你也不能传递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")
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容

  • 早晨七点半,床头的闹铃如期的响起,伸手摁掉那持续做响的铃声,撑着双眼对着白色的天花板发呆,脑里一片空白,从窗边窜进...
    桃之夭夭未命名阅读 311评论 0 0
  • 《金瓶梅》,包罗万象,涵盖百科。作者之博通学识,可明证其非为等闲之辈。由其中命理之学,便可窥得冰山一角。书中占卜,...
    司香尉阅读 4,779评论 4 106
  • 小阿我特别喜欢动物,尤其是狗狗,简直爱到不行的那种,或许是从小跟狗狗一起长大的缘故吧,对狗狗的感情特别的深。 ...
    小阿的树洞阅读 207评论 0 0
  • 【一】 周末在家陪娃玩。 娃爹来了,敲门。 儿子乐呵呵的去给爸爸开门,他爸爸进来在门口换鞋子的空,儿子快速跑回来趴...
    龙翠阅读 313评论 5 2
  • 桶排序虽然快但是浪费空间,冒泡排序虽然解决了空间问题,但是时间效率很低。下面我们就讨论一下快速排序。引用百度百科:...
    Airycode阅读 321评论 0 0