go-database-sql

原文http://go-database-sql.org/overview.html

在Go操作sql或者sql-like的数据库是通过database/sql包。它为面向行的数据库提供了轻量的接口。本章主要讲如何使用这个package的各各方面。

为什么我们需要在这里讲这些内容?package的文档只是告诉你每一个函数是什么,但它没有告诉你如何使用这个包。

Overview

在Go中,为了访问数据库,你可以使用sql.DB. 你使用此类型创建statement, transactions, execute quires, fetch results.

首先你需要知道的是一个sql.DB类型,它不是一个数据库连接 database connection. 它也不能映射到任何特定数据库中的"database"或者 "schema"等概念. 它是一个数据库接口和存在性的抽像表示。这种抽像是多种多样的,它可以是本地文件,通过网络连接访问,或者在内存和进程中。

sql.DB为了在后端执行了许多重要任务

  • 它通过驱动,打开和关闭底层的数据库连接
  • 如果有需要,它可以管理连接池

sql.DB的抽像的设计,是让不用担心如何来管理底层的数据存储的并发访问。当你使用一个连接来执行一个任务时,这个连接被标记。然后,当它没有在使用时,它会被返回到一个有效的连接池中。这导致的一个结果是,如果你没有释放连接到连接池,则会导致db.SQL打开许多的连接。潜在的会导致资源的耗尽(太多的连接,太多打开的文件句柄,缺乏有效的网络端口),我们将在之后进行讨论。

当你创建完sql.DB之后,你可以使用它来查询它所表示的数据库,以及创建statement和transactions.

Importing a Database Driver

为了使用database/sql, 你首先需要这个包,还有你想要使用的数据库的驱动。

通常,你不需要直接使用到驱动包,虽然有些驱动会鼓励你那么做。(在我们看来,这是一个坏主意). 尽可能的在你的代码使用database/sql中定义的类型。这可以使用你的代码依赖于某个驱动,这样你可以以最小的代码修改,改变底层的数据库驱动。同时,还可以熟练使用Go的习惯来操作数据库,而不是特定驱动作者的习惯。

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

在这里,我们以匿名的方式加载数据库驱动(package前面是_), 在这下面,驱动向database/sql中注册自己. 通常是在init函数中调用sql.Register(name string, driver driver.Driver). 这样你就可以访问数据库了。

Accessing the Database

现在你已经加载的了driver包,就可以准备好创建一个数据库对像,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()
}
  1. sql.Open的第一个参数是数据库驱动的名称。这个字符串是driver注册自己到database/sql时使用的名字。通常跟包的名字是一样的。 比如 mysql表示的是githb.com/go-sql-driver/mysql. 有的驱动没有遵循这种惯例,而是使用数据库的名字,比如sqlite3 表示githb.com/mattn/go-sqlite3,postgres表示github.com/lib/pq.

  2. 第二个参数是驱动指定的语法,告诉驱动如何访问底层的数据存储。在这个例子中,我们连接到本地的Mysql服务器的hello数据库。

  3. 你应当检查和确认所有的database/sql操作的返回值。有一些特殊情况,不需要对返回值进行检查,我们在之后讨论。

  4. 调用db.Close()关闭,如果sql.DB不在需要使用

跟我们想的不一样,sql.Open()不会跟数据库建立任何的连接,也不会校验数据库连接的参数是否正确。在这里,它只是为之后的使用,简单的准备了一个数据库抽像。第一次实际连接到底层的数据库的连接会被惰性创建,即在首次需要的时候建立。如果你需要检查数据库是否有效和可访问(比如,确认你可以建立网络连接和登录),你可以使用db.Pint()来做,并且记得检查errors:

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

虽然在我们完成数据库操作会,会使用Close()来关闭连接,但是sql.DB对像被调计为long-live. 请不要频繁的使用Open()和Close()。只需要为每一个要使用到的数据存储对像创建一次。并且保持它到程序不在访问这个数据存储对像。 如果需要,可以通过传递的访问,或者定义为全局变量。在一个short-lived函数中,不要使用Open()和Close(), 而是向它传递一个打开的sql.DB.

如果你不把sql.DB作为long-lived对像看待,你可能会面临许多问题,比如poor重用和共享连接。耗尽可用的网络资源,或者很多的连接停留在TCP的TIME_WATI状态中。

现在你可以使用sql.DB对像了

Retrieving Result Sets

以下是数据库检索结果的常用操作:

  1. 执行一条查询,返回多行
  2. 多次重用的prepare statement.可以多次执行,和销毁它
  3. 执行一次性语句,而不用为重复使用它而准备
  4. 执行一条查询,返回单个的行。这是对特殊情况的一种捷径

database/sql中函数的名字有特定意义,如果一个函数名,包含Query, 它被设计用于,向数据库请求一个问题,并且返回多行数据集,即使它返回空行。如果语句不需要返回空行,则不应该使用Query函数,而应该使用Exec()

Fetching Data from the Database

让我们看一个例子,它是如何从查询数据库的,以及操作返回的结果。我们将查询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)
}

上面的代码发生了什么:

  1. 我们使用db.Query()向数据库发送了一个查询,然后检查是否发生了错误
  2. 我们defer rows.Close()手动关闭,因为我们不清楚是否有自动关闭(Next反回false时,会自动关闭). 这非常重要
  3. 通过rows.Next遍历rows.
  4. 通过 rows.Scan()将每一行中的列,读取到变量.
  5. 通过rows.Err()检查在遍历时是否有发生错误。

在Go语言中,上面的方式几乎是唯一的方法。你不能像获得一个map那样,获得一行. 这是因为一切都是强类型。你需要为变量创建正常的类型,并且传递这些变量的指针。

以下是很容易出错的部分.

  • 你需要在for rows.Next()之后检查错误。如果有一个错误在循环期间发生,你需要知道这个错误。不要假设整个循环会等到你处理完所有的行。
  • 第二条,只要有打开的结果集(rows),底层的连接就是忙碌状态,所以不能用于其它查询. 但连接池是有可以的。如果你使用rows.Next()遍历所有的行,在最后一行,rows.Next()将会遇到内部的EOF error, 并且调用rows.Close(). 但如果因为其它原因退出,则rows将不会关闭,连接会保持打开。这是一个简单耗尽资源的方法。
  • rows.Close()是一个无效的操作,如果rows已经关闭。所以你可以多次调用它。可是在调用之前,我们需要先检查是否发生错误,只有在没有发生错误时,才能调用,否则会有运行时异常。
  • 你应该一直使用defer rows.Close()
  • 不要在循环中使用defer.一条deferred语句,只会在函数退出时执行,所以一个长时间运行函数,不应该使用defer. 如果你这么做了,会累积很多的内存,变得很慢。如果你在一个循环中要重复查询并且处理结果集,你应当明确的调用rows.Close(),而不是使用defer.

How Scan() Works

当你遍历rows, 并且将它们扫描到目标变量,Go会在幕后,为你完成数据类型的转换。这主要是基于目标变量的类型。这就可以简化你的代码,避免重复的工作。

举例来说,假设数据库表的例,定义的字符串,比如varchar(45) or 类似的。但你也知道,table中也会包含数字。如果你传递一个指针给string.Go将复制字节到这个字符串。你也可以使用strconv.ParseInt()或者类似的函数,将一个值转换为数字。你即要对SQL操作是否有错误发生,还需要对解析数字时,发生的错误进行校验。这很凌乱,也很繁琐。

或者,你可以是向Scan()传递一个指针,Go将决定并且调用strconv.ParseInt(). 如果在转换的过程中发生错误,调用 Scan()时,会返回这个错误。你的代码会变得整洁,而清楚,这也是什么推荐使用database/sql的原因之一。

Preparing Queries

一般来说,你应该准备一个查询,可以被多次使用。这称为prepared statement. 它可以有占位符,在执行statement, 向这些占位符传递参数。这要比连接字符串好很多。还有其它的原因,那就是可以避免SQL注入攻击.

在MySQL中,参数的占位符是?, 在PostgreSQL是$N, N表示数字。SQLite两者都可以接收。在Oracle的形式为:param1. 我们在这里我们使用?

stmt, err := db.Prepare("select id, name 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()实际上执行prepares, executes, close a prepared statement. 这三个操作到数所库有三个round-trips. 如果你没有注意到,你可以在应用程序中提高三倍的数量的数据库交互来做测试。有的驱动可以避免这些操作,但不是所有的都这样。可以通过 prepared statements 了解更多

Single-Row Queries

如果查询只返回单行,你可以使用以下的代码

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()之后,然后返回。你也可以在prepared statement中调用QueryRow().

stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
    log.Fatal(err)
}
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)

Modifying Data and Using Transactions

现在我们看看如何修改数据和使用事务。如果你习惯了使用一个"statement"对像来获取以及更新数据的编程语言,这两者只是有人为的不同,但在Go语言里,这两者是有本质的不同。

Statements that Modify Data

使用prepared statement的Exec()函数,可以执行INSERT, UPDATE, DELETE,或者其它语句,它们都不会返回行。下面的例子向你展示如何插入一行,以及检查操作的元数据。

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
    log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
    log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
    log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
    log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)

执行这个statement会会产生一个sql.Result类型的对像,它让我们可以访问statement的元数据:last inserted ID(数据库自动产生后的返回)和影响的行数。

如果你不想知道result. 你只需要检查err。但以下两个语名是相同的嘛?

_, err := db.Exec("DELETE FROM users")  // OK
_, err := db.Query("DELETE FROM users") // BAD

回答肯定是no, 它们没有做相同的事情,而且你不应该像这样使用Query(). Query()函数会返回一 个sql.Rows类型,你保留了数据库的连接,直到sql.Rows关闭。由于我们在这里没有读取数据,这个连接也就不能被使用。在上面的例子中,连接将不能被释放。垃级收集器最终会为你关闭底层的net.Conn.但这会花费很长的时间。此外database/sql包还会在它的连接池中追踪这个连接,它希望你能释放这个连接,让这个连接返回到连接池,可被再次使用。此反模式也是耗尽资源的最好方法.

使用事务

在Go语方中,一个transaction的本质是保留了连接到数据存储的连接对像。它可以让你做所有的操作,但能够保证所有的执行都是在同一个连接上。

你可以调用db.Begin()开始一个事务,同时返回Tx对像和一个err, 通过调用Tx上的Commit()或者Rollback()方法来关闭一个事务。在后台,Tx从连接池中获得一个连接。并且保留它,只供这个事务使用。Tx中的方法,跟database中的方法是一样的,比如Query()等等。

在一个transation中创建的prepared statement只能绑定到这个事务。你可以查看Prepare statements了解更多

你不能在事务中的SQL语句中使用BEGIN和COMMIT,它会有以下坏的结果:

  • 因为没有使用Tx的Commit,Tx依然还是打开状态,这样从连接池中获得的连接不会被释放。
  • 数据库的状态跟Go变量所示的状态会不同步
  • 在一个事务中,你可以相信,所有的查询都是在同一个连接中。而实际上,Go会为你无形中创建多个连接,有的语句它不是事务的一部分。

当你在使用一个事务时,你应该小心不要使用到Db中的变量。确保所有的调用都是在Tx的变量,Db不是一个事务,只有Tx是。如果你调用了db.Exec()或者类似的,它们将超出你事务的作用域。它们是在另一个连接上。

如果你需要使用到修改连接的状态的语句,你也需要使用到Tx, 即使它不是一个事务。比如:

  • 创建一个临时表,它只能在当个连接中可见
  • 设置一个变量,比如MySQL的 SET @var := somevalue语法
  • 改变连接的可选项,比如字符集和timeouts. 如果你需要做这些事情,你需要将它们绑定到单个的连接中,在Go中,只有一个方法,那就是使用Tx.

Using Prepared Statements

Prepared statements有很多益处:安全性,效率性和方便性。但你在使用时会发现,它们的实现有点不同,特别是在database/sql的内部交互中。

Prepared Statements And Connections

在数据库级别,一个prepared statement是绑定在单个的数据库连接中的。一般的流程是,客户端将SQL有占位符的语句发送给服务器,让它准备好。服务器返回一个语句的ID, 然后,客户端执行语句时发送这个ID和参数。

但在Go语方中,database/sql package没有向用户直接暴露连接。你不能在一个连接中准备一个语句。你只能使用Db或者Tx. 并且,database/sql有一些便利的行为,比如自动重试。出于这些原因,prepared statement和connections的底层联系,由驱动级别完成。

以下是它工作原理:

  1. 当你准备一个语句(prepare a statement), 它会在pool中的一个与数据库的连接,并在这单个连接中准备
  2. Stmt对像会记住哪一个连接被使用
  3. 当你要执行Stmt时,它尝试使用这个连接。如果它不可用,比如被关闭或者忙于其它事情,它从连接池中获得另一个连接,并且跟数据库的其它连接,重新re-prepare statement

由于statement在原始连接忙的情况下,可以被re-prepared。可能会导致数据库的高并发使用,这就造成有大量的连接在忙,创建大量的prepared statements. 出现statement泄漏。statement的准备和重准备的数量比你想像的要多,甚至超出了服务器端对statement数量的限制.

避免使用Prepare Statement

Go会在幕后为你创建Prepare statement.举个简单的例子db.Query(sql, param1, param2),它就会先创建服务器准备一个statement. 然后执行参数,最后在关闭这个statement.

有的时候,prepare statement并不是你想要的,比如,以下的原因:

  1. 数据库不支持prepare statement. 举例来说,你使用MySQL驱动,你也可以连接MemSQL和Sphinx(这两种数据库支持MySQL连接协议)。但它们不支持"binary"协议,这个二进制协议包含了prepared statements. 所以它们会失败,并且让你困惑。
  2. 语句没有重用的价值。同时安全问题可以通过其它的方式处理。性能问题也就不重要了。对于这一点,你可以看看 VividCortex blog

    VividCortext是监测数据库的工具, 上面这个blog是讨论,如何优化查询预处理语句的。当你设计的预处理语句被多次使用时,那么你就可以使用预处理,但如果你的语句只执行一次,然后关闭它。这就会有三次与服务器的网络来回(发送预处理语句,返回statement ID给客户端,发送参数). 这就降低了整个的性能,而不是提高。

如果你不想使用prepared statement, 你需要使用到fmt.Sprint()或者类似的函数来组装SQL, 然后作为单个的参数传递给db.Query()或者db.QueryRow(). 同时你的驱动必须要支持Plaintext查询的执行。这些在Go1.1中通过Execer和Queryer接口实现。

Prepared Statements in Transactions

预处理语句也可以在Tx中创建,并绑定到这个Tx中。所以之前创建的预处理语句不适用于Tx.当你操作一个Tx对像时,你实现上操作的是它底层的连接。

这意味着,在Tx中创建的预处理语句,不能被分开使用。同样,在Db中创建的预处理语句也不能在事务中使用。因为它们绑定到不同的连接中。

为了使用在Tx对像之外的预处理语句,你可以使用Tx.Stmt(),它会从事务之外的预处理语句中创建一个新的事务专用的语句。它采用现有的prepared statement, 设置它到事务的连接。并且每次执行语句时,重新repreparing所有语句。这种行为和实现是不可取的,甚至在database/sql的源代码中,还是TODO来改进它。建议不要使用这种方式。

在一个事务中使用预处理语句,需要注意以下几点:

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close() // danger!
for i := 0; i < 10; i++ {
    _, err = stmt.Exec(i)
    if err != nil {
        log.Fatal(err)
    }
}
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}
// stmt.Close() runs here!

在Go1.4之前,关闭一个*sql.Tx, 会释放分配给它的连接,返回到pool, 但deffered调用,来关闭一个预处理语句,会在此时调用。这就可能导致并发访问底层的连接,读取到连接的状态不一致。如果你使用Go 1.4或之后的版本,你应该在提交事务或者回滚之前关闭。

Parameter Placeholder Syntax

占位符的语法是根据数据库来指定的,以下是MySQL, PostgreSQL, Oracle的比较.

MySQL PostgreSQL Oracle
WHERE col = ? WHERE col = $1 WHERE col = :col
VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)

Handling Errors

database/sql类型返回的最后的一个值是一个error. 你应该检查这些errors, 而不是忽略它们。

有些地方的error比较特殊,或者有一些额外的东西,你需要知道。

Errors From Iterating Resultsets

考虑以下的代码

for rows.Next() {
    // ...
}
if err = rows.Err(); err != nil {
    // handle the error here
}

rows.Err()的结果可能是在rows.Next()循环中发生的各种错误。这个循环可能会因为某种原因为退出,而不是正常的退出。所以你需要检查循环是否为正常的终止。异常终止是会自动调用rows.Close(), 即使多次调用它也是无害的。

Errors From Closing Resultsets

你应该明确的关闭sql.Rows, 如果你过早的退出循环,比如之前提到的,循环正常退出或者发生错误,都会自动closed. 但你可能有以下的错误做法。

for rows.Next() {
    // ...
    break; // whoops, rows is not closed! memory leak...
}
// do the usual "if err = rows.Err()" [omitted here]...
// it's always safe to [re?]close here:
if err = rows.Close(); err != nil {
    // but what should we do if there's an error?
    log.Println(err)
}

rows.Close()返回的错误跟一般的规则不同,它用于捕获和检查所有的数据库操作发生的错误。如果rows.Close()返回一个错误,对你来说,你会不清楚能做什么。 记录错误信息和异常,是你唯一可以做的。如果这些不是想要的,或许你应该忽略它们。

Errors From QueryRow()

考虑以下检索单行的代码

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)

如果没有id为1的用户怎么办?则没有一行结果,可以scan到name变量。那么这发生了什么?

Go为此定义了专门的error常量,称为sql.ErrNoRows, 当QueryRow()调用后返回的结果为空时调用。在多数情况下,这需要特殊的处理。一个空的结果在应用程序的代码中,不会被认为是一个错误。并且如果你不检查这个错误是否为这个指定的常量,你将会引发你不期望的应用程序错误。

从查询中发生的错误,会延迟到直到Scan()调用,然后在返回。以上的代码,最好写成下面的方式

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows {
        // there were no rows, but otherwise no error occurred
    } else {
        log.Fatal(err)
    }
}
fmt.Println(name)

有人可能会问,为什么空结果集会被认为是一个错误。原因是在于QueryRow()方法需要使用这种特殊的方法,让它的调用者知道是否有找到一行。如果没有它,Scan()将什么也不会做,而你也不会知道,你的变量没有从数据库中得到任何值。

当你没有使用QueryRow()时,你不应该不会遇到这个错误。如果你在其它地方遇到了这个错误,那就说明你做错了什么。

Identifying Specific Database Errors

rows, err := db.Query("SELECT someval FROM sometable")
// err contains:
// ERROR 1045 (28000): Access denied for user 'foo'@'::1' (using password: NO)
if strings.Contains(err.Error(), "Access denied") {
    // Handle the permission-denied error
}

用上面的方法来识别错误不是最好的方法,上面的字符串值,依赖于数据库服务器使用什么语言来发送错误消息。所以需要使用error number来标识错误信息。

因为这不是database/sql的一部分,不同的驱动有者不同的机制。在MySQL驱动中,你可以使用以下的代码

if driverErr, ok := err.(*mysql.MySQLError); ok { // Now the error number is accessible directly
    if driverErr.Number == 1045 {
        // Handle the permission-denied error
    }
}

MySQLError类型是由特定的驱动器提供,.Number也会在不同的驱动之间而不同。 number的值是从MySQL's的错误消息中获得,它由数据库指定,而不是由驱动指定。

上而的代码还是比较丑,有的驱动提供了错误标识符,比如Postgres pq的驱动就提供了一个error.go. 对于MySQL来说,MySQL error numbers maintained by VividCortex[https://github.com/VividCortex/mysqlerr]. 所以上面的代码可以写成以下的形式

if driverErr, ok := err.(*mysql.MySQLError); ok {
    if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
        // Handle the permission-denied error
    }
}

Handling Connection Errors

如果你连接到数据库的连接被dropped, killed或者有一个错误,那应该做什么?

当这些情况发生时,你不需要写任何的逻辑。database/sql会帮你处理。如果你执行一个查询或者其它的语句时,底层的连接失败。Go将重开一个新的连接(或者仅从pool中获得一个其它的连接),然后重试。

可是,在error的处理中,也有一些意想不到。有的错误类型会当其它错误条件发生时,才会发生。这跟特定的驱动有关. 举一个MySQL驱动的例子。它会使用KILL来取消一个不需要的语句(比如长时间的查询).

Working with NULLs

空列是烦人的,导致很多丑陋的代码。如果可以,应该避免它们。如果不能避免,你将需要使用database/sql中的特殊类型来处理它们。或者你自己进行定义。

以下的类型可以有空类型,booleans, strings, integers, floats.

for rows.Next() {
    var s sql.NullString
    err := rows.Scan(&s)
    // check err
    if s.Valid {
       // use s.String
    } else {
       // NULL value
    }
}

如果你需要定义自己的类型来处理NULLs, 你可以复制sql.NullString来实现.

Working with Unknown Columns

Scan()函数要求你传递明确数量的变量。如果你不知道,查询会返回什么?那应该怎么办。

如果你不知道有多少列会在查询中返回,你可以使用Columns()来找到列的名字列表。你可以检查这个列表的长度,看看有多少返回的结果有多少列。你也可以向Scan()会传递一个slice. 比如有此MySQL分支版本,可以使用SHOW PROCESSLIST命令,返回不同的列。所以你必须做好准备,否则你会引发一个错误。

cols, err := rows.Columns()
if err != nil {
    // handle the error
} else {
    dest := []interface{}{ // Standard MySQL columns
        new(uint64), // id
        new(string), // host
        new(string), // user
        new(string), // db
        new(string), // command
        new(uint32), // time
        new(string), // state
        new(string), // info
    }
    if len(cols) == 11 {
        // Percona Server
    } else if len(cols) > 8 {
        // Handle this case
    }
    err = rows.Scan(dest...)
    // Work with the values in dest
}

如果你不清楚列的类型,你可以使用sql.RawBytes.

cols, err := rows.Columns() // Remember to check err afterwards
vals := make([]interface{}, len(cols))
for i, _ := range cols {
    vals[i] = new(sql.RawBytes)
}
for rows.Next() {
    err = rows.Scan(vals...)
    // Now you can check each element of vals for nil-ness,
    // and you can use type introspection and type assertions
    // to fetch the column into a typed variable.
}

The Connection Pool

在database/sql包中有一个基础的连接池。它没有提供控制和检查它的能力。但有的事情,你会发现非常有用。

  • 连接池意味着,在单个数据库上,可以执行两个连续的语句。它会打开两个连接,并且独立的执行它们。程序员会经常为此困域,它们的代码不像预期的那样。比如, LOCK TABLE之后跟一个INSERT, 插入应该被阻止。但是其它INSERT所在的连接可能没有表锁,所以不会被阻止。
  • 连接只在需要的时候创建,在连接池中没有空连接。
  • 默认情况下,没有对连接的数量进行限制。如果你一次想做大量的事情,你可以创建任意数量的连接。这可能会引起数据库返回一个错误,比如"too many connections".
  • 在Go 1.1或更新的,你可以使用db.SetMaxIdleConns(N)来限制池中idle connections的数量。不过,这并非限制pool的大小。
  • 在Go 1.2.1或更新的,你可以使用db.SetMaxOpenConns(N)来限制总共打开连接到数据库的connections. 但是这会导致死锁的bug.
  • Connection可以被很快的回收,通过db.SetMaxIdleConns(N)设置空闲连接为一个高的数字。可以减少流失,并有助于保持连接的重用。
  • 保持一个连接的空闲时间太长,会引发许多的问题(比如在Microsoft Azure中的MySQL 问题https://github.com/go-sql-driver/mysql/issues/257)。如果你因为连接的空闲太长,导致连接超时,你可以设置db.SetMaxIdleConns(0).

反模式与限制

Resource Exhaustion

以下的方式会导致资源枯竭:

  • 在一个函数内打开一个数据库后,在函数结束时关闭数据库。
  • 没有读取完所有的行,或使用rows.Close(), 连接不会返回到pool.
  • 使用Query()语句,而又没有获得它的返回行。连接不会返回到pool.
  • 未能意识到预处理语句是如何工作的,它可能会导致额处的数据库活动。

Large uint64 Values

这是一个意外的错误。你不能向一个语句中传递一个大的无符号整行。如果这个参数的高位被设置了。

_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error

这就会有一个错误。如果你在使用uint64位的值时需要小心一些,在这些数字比较小时,没有问题,当随着时间的增长,就会有错误的发生。

Multiple Result Sets

Go驱动不支持多个结果集从单个查询中返回。并且没有任何一个计划来做这个事情,虽然在github中有一些feature request要求有批量操作,比如批量复制。

之意味着,一个存储过程反回的多个结果集,将不能正常工作。

调用存储过程

调用存储过程是由驱动决定的。但在MySQL驱动中,它还不能调用。虽然你想使用下面的方式来调用一个简单的存储过程。

err := db.QueryRow("CALL mydb.myprocedure").Scan(&result) // Error

实际上,这是不允许的。你会获得一个Error 1312: PROCEDURE mydb.myprocedure can’t return a result set in the given context. 这是因为MySQL期望连接被设置为multi-statement模式。即使是单个结果。

Multiple Statement Support

database/sql没有明确的支持多语句。以下行为是不允许的

_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // Error/unpredictable result

服务器是可以解析这条语句,但是返回的错误,只会包含第一条或者两条的执行错误。

相似的,在transation中也不没有批处理语句。每一条语句,都必须是连续的。结果中的资源,比如Row or Rows,必须scanned 或者closed. 底层的连接被释放,下条语句才能被使用。如果不是在一个事务中,你完成可以执行一个行,循环rows,并且在循环中进行数据库的查询

rows, err := db.Query("select * from tbl1") // Uses connection 1
for rows.Next() {
    err = rows.Scan(&myvariable)
    // The following line will NOT use connection 1, which is already in-use
   db.Query("select * from tbl2 where id = ?", myvariable)
}

但是在事务中,这是不能执行的。

tx, err := db.Begin()
rows, err := tx.Query("select * from tbl1") // Uses tx's connection
for rows.Next() {
    err = rows.Scan(&myvariable)
    // ERROR! tx's connection is already busy!
   tx.Query("select * from tbl2 where id = ?", myvariable)
}