Lazyboy果然又lazy了,一周没写东西。

GO DATABASE/SQL Tutorial 

Handling Errors

几乎所有对 database/sql类型的操作都会有一个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。如果循环正常退出或发生错误,它将自动关闭,但我们可能会错误地执行此操作:

for rows.Next() {
	// ...
	break; // rows没有关闭,内存泄漏
}

if err = rows.Close(); err != nil {
	log.Println(err)
}

rows.close()返回的错误是常规规则的唯一例外,最好捕获并检查所有数据库操作中的错误。如果rows.close()返回错误,我们也不清楚应该怎么做。记录错误消息或panic可能是唯一明智的选择,如果这样不合适,那么也许应该忽略错误。

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定义了一个特殊的错误常量,称为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()方法需要使用这种特殊情况,以便让调用者区分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
}

不过这不是最好的办法。比如,根据服务器用来发送错误消息的语言,字符串值可能会有所不同。比较错误的数量来确定具体的错误是什么是种更好的选择。

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字段可能有所不同。但是,这个数字的值取自MySQL的错误消息,因此是特定于数据库的,而不是特定于驱动程序的。

代码很难看,我们也不希望突然出现无意义的数字1045来代表某些状态。一些驱动程序提供了一个错误标识符列表。有一个外部的包 MySQL error numbers maintained by VividCortex. 使用这样的列表,可以更好地编写上述代码:

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

Handling Connection Errors

如果与数据库的连接被删除、终止或有错误怎么办?

发生这种情况时,不需要实现任何逻辑来重试失败的语句。作为数据库/SQL中连接池的一部分,处理失败的连接是内置的。如果执行查询或其他语句,并且基础连接出现故障,Go将重新打开一个新连接(或从连接池中获取另一个连接),然后重试,最多10次。

然而,可能会有一些意想不到的后果。当发生其他错误情况时,可能会重试某些类型的错误。这也可能是特定于驱动程序的。MySQL驱动程序中出现的一个例子是,使用kill取消不需要的语句(例如长时间运行的查询)会导致语句被重试10次。

 

Working with NULLs

可以为空的列是令人讨厌的,并且会导致许多丑陋的代码。如果可以的话,避开他们。如果没有,则需要使用数据库/SQL包中的特殊类型来处理它们,或者定义自己的类型。

有可为空的布尔值、字符串、整数和浮点值的类型。以下是使用方式:

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

空类型的限制,以及在需要更具说服力时避免空列的原因:

  1. 没有SQL.NullUInt64或SQL.NullYourFavoriteType。你需要为此定义你自己的。

  2. 可空性可能很棘手,而且不会出现面向未来的问题。 如果认为某些内容不会为空,但是出错了,那么程序将崩溃,或许这种情况很少,以至于在发送之前不会捕获错误。

  3. Go的一个优点是为每个变量都提供了一个有用的默认零值。这不是空值的工作方式。

如果需要定义自己的类型来处理空值,可以复制SQL.NullString的设计来实现这一点。

如果不能避免在数据库中使用空值,那么大多数数据库系统支持的另一项工作就是coalesce()。下面这样的内容可能是可以使用的,而不需要引入大量的sql.null*类型。

// COALESCE()处理空值
rows, err := db.Query(`
	SELECT
		name,
		COALESCE(other_field, '') as otherField 
	WHERE id = ?
`, 42)

for rows.Next() {
	err := rows.Scan(&name, &otherField)
	// ..
	// If `other_field` was NULL, `otherField` is now an empty string. This works with other data types as well.
}

 

Working with Unknown Columns

Scan()函数要求精准传递正确数量的目标变量。如果我们不知道查询将返回什么结果呢。

如果不知道查询将返回多少列,可以使用columns()查找列名称列表。您可以检查此列表的长度以查看有多少列,并且可以使用正确数量的值将切片传递给scan()例如,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中有个基础的连接池。没有太多的能力来控制、检查它,但我们可能会发现一些有用的信息:

  • 连接池意味着在一个数据库上执行两个连续语句可能会打开两个连接并分别执行它们。对于程序员来说,为什么他们的代码行为不正确是很常见的。例如,后面跟着insert的锁表行为可以被阻塞,因为insert所处的连接不包含表锁。
  • 在需要时创建连接,池中没有可用连接。
  • 默认情况下,连接数量没有限制。如果你试图同时做很多事情,你可以创建任意数量的连接。这可能导致数据库返回错误,例如“连接太多”。
  • 在Go 1.1或更高版本中,可以使用db.setMaxIdleConns(N)来限制池中的空闲连接数。不过,这并不限制池的大小。
  • 在Go 1.2.1或更高版本中,可以使用db.SetMaxOpenConns(N)限制到数据库的总打开连接数。不幸的是,死锁错误阻止了db.SetMaxOpenConns(N)在1.2中的安全使用。
  • 连接的回收速度相当快。使用db.setMaxIdleConns(n)设置大量空闲连接可以减少这种干扰,并有助于保持连接以供重用。
  • 长时间保持连接空闲可能会导致问题(如Microsoft Azure上的MySQL出现此问题)。如果由于连接空闲时间过长而导致连接超时,请尝试db.setMaxIdleConns(0)。
  • 还可以通过设置db.SetConnMaxLifetime(duration)来指定可以重用连接的最大时间量,因为重用长寿命连接可能会导致网络问题。这会延迟关闭未使用的连接,即可能延迟关闭过期的连接。

 

Surprises, Antipatterns and Limitations

虽然一旦习惯了数据库/SQL就很简单了,但是我们可能会惊讶于它所支持的用例的微妙性。这在Go的核心库中很常见。

Resource Exhaustion

正如之前提到的,如果不按预期使用database/sql,通常会消耗一些资源或阻止它们被有效地重用,这肯定会带来麻烦:

  • 打开和关闭数据库会导致资源耗尽。
  • 无法读取所有行或使用Close()保留池中的连接。
  • 对不返回行的语句使用query()将保留来自池的连接。
  • 如果不知道prepared语句如何工作,可能会导致大量额外的数据库活动。

Large uint64 Values

这是一个令人惊讶的错误。如果设置了大的无符号整数的高位,则不能将其作为参数传递给语句:

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

这将引发一个错误。如果使用uint64值,则要小心,因为它们开始时可能很小,并且不会出错,但会随着时间的推移而增加,并开始抛出错误。

Connection State Mismatch

有些事情会改变连接状态,这可能会导致两个原因的问题:

  1. 一些连接状态,例如是否在事务中,应该通过go类型来处理。
  2. 可能假设查询在一个连接上运行,而它们不是。

例如,用USE语句设置当前数据库对于许多人来说是一件典型的事情。但是在Go中,它只影响运行它的连接。除非在事务中,否则我们认为在该连接上执行的其他语句实际上可能在从池中获取的不同连接上运行,因此它们不会看到这些更改的效果。

此外,在更改了连接之后,它将返回连接池,并可能污染其他代码的状态。这也是为什么不应该将begin或commit语句作为SQL命令直接发出的原因之一。

Multiple Statement Support

数据库/SQL没有显式的多语句支持,这意味着它的行为依赖于后端:

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

服务器可以任意执行这个语句,包括返回错误、只执行第一条语句或同时执行这两条语句。

同样,在事务中也没有方法批处理语句。事务中的每个语句都必须连续执行,并且结果中的资源(例如一行或多行)必须被扫描或关闭,以便基础连接可以供下一个语句使用。这与不处理事务时的常规行为不同。在这种情况下,完全可以执行查询、循环行,并在循环中对数据库进行查询(这将在新连接上发生):

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)
}

但是,Go并不能阻止你尝试。因此,如果在第一个语句释放其资源并在其自身之后清除之前尝试执行另一个语句,则可能最终导致连接损坏。这也意味着事务中的每个语句都会导致到数据库的一组单独的网络往返。

 

这个Tutorial中对大多数情况做了解释,或者说理论上的说明,给了一些语句片断让我们理解,具体操作还需要我们自己多敲一敲,学习实际场景中的go for mysql。

 

参考文献:

记录每天解决的小问题,积累起来去解决大问题。