go-database-sql
在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()
}
sql.Open的第一个参数是数据库驱动的名称。这个字符串是driver注册自己到database/sql时使用的名字。通常跟包的名字是一样的。 比如 mysql表示的是githb.com/go-sql-driver/mysql. 有的驱动没有遵循这种惯例,而是使用数据库的名字,比如sqlite3 表示githb.com/mattn/go-sqlite3,postgres表示github.com/lib/pq.
第二个参数是驱动指定的语法,告诉驱动如何访问底层的数据存储。在这个例子中,我们连接到本地的Mysql服务器的hello数据库。
你应当检查和确认所有的database/sql操作的返回值。有一些特殊情况,不需要对返回值进行检查,我们在之后讨论。
- 调用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
以下是数据库检索结果的常用操作:
- 执行一条查询,返回多行
- 多次重用的prepare statement.可以多次执行,和销毁它
- 执行一次性语句,而不用为重复使用它而准备
- 执行一条查询,返回单个的行。这是对特殊情况的一种捷径
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)
}
上面的代码发生了什么:
- 我们使用db.Query()向数据库发送了一个查询,然后检查是否发生了错误
- 我们defer rows.Close()手动关闭,因为我们不清楚是否有自动关闭(Next反回false时,会自动关闭). 这非常重要
- 通过rows.Next遍历rows.
- 通过 rows.Scan()将每一行中的列,读取到变量.
- 通过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的底层联系,由驱动级别完成。
以下是它工作原理:
- 当你准备一个语句(prepare a statement), 它会在pool中的一个与数据库的连接,并在这单个连接中准备
- Stmt对像会记住哪一个连接被使用
- 当你要执行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并不是你想要的,比如,以下的原因:
- 数据库不支持prepare statement. 举例来说,你使用MySQL驱动,你也可以连接MemSQL和Sphinx(这两种数据库支持MySQL连接协议)。但它们不支持"binary"协议,这个二进制协议包含了prepared statements. 所以它们会失败,并且让你困惑。
- 语句没有重用的价值。同时安全问题可以通过其它的方式处理。性能问题也就不重要了。对于这一点,你可以看看 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)
}
Related Reading and Resources
- http://golang.org/pkg/database/sql/
- http://jmoiron.net/blog/gos-database-sql/
- http://jmoiron.net/blog/built-in-interfaces/
- The VividCortex blog, e.g. transparent encryption