Go数据库访问

本文围绕Go语言访问数据库展开,介绍了sql.DB的概念与作用,阐述导入数据库驱动、创建数据库对象的方法。详细讲解检索结果集、修改数据、使用事务、异常处理等操作,还提及处理NULL值、未知列以及连接池管理等内容,助于开发者掌握Go操作数据库技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 概述

Go中访问数据库,您需要用到sql.DB:您可以使用它来创建语句【statements 】和事务【transactions】、执行查询和获取结果。

但是您首先需要谨记的是:sql.DB并不是一个数据库连接【 database connection】,而且它也不映射到任何特定数据库软件的“database”或“schema”概念。它是对界面和数据库存在(可以像本地文件一样变化)的抽象,既可以通过网络连接访问,也可以在内存和进程中访问。

sqp.DB Doc :DB是表示0个或多个底层连接池的数据库句柄。它对于多个goroutines的并发使用是安全的。

sql.DB在后台为你执行一些重要的任务:

  1. 通过驱动(Driver),创建到到底层的数据库的连接(Connection)
  2. 它根据需要管理连接池

sql.DB抽象设计的目的是让您不必担心如何管理对底层数据存储的并发访问。当您使用到底层数据库的连接【Connection】执行任务时,该连接【Connection】会被标记为“正在使用【 in-use 】”,然后当我们不再使用它时,该连接【Connection】会放回到可用连接池中。这样做的一个后果是,如果用户在使用完连接【Connection】后忘记将其释放回连接池,就会导致db.SQL打开大量连接,最终可能会耗尽资源(太多连接、太多打开的文件句柄、缺少可用的网络端口等)。稍后我们将对此进行更多讨论。

创建完sql.DB后,您可以使用它来查询它所表示的数据库,以及创建查询以及事物。

2.导入数据库驱动

使用数据库时,除了database/sql包本身,还需要引入想使用的特定数据库驱动。

通常不应该直接使用驱动程序包,尽管有些驱动程序鼓励您这样做(在我们看来,这通常是个坏主意)。相反,如果可能的话,代码应该只引用在database/sql中定义的类型。这有助于避免代码依赖于驱动程序,这样您就可以用最少的代码更改来更改底层驱动程序(从而更改正在访问的数据库)。它还会强制您使用Go习惯用法而不是特定驱动程序作者可能提供的特殊习惯用法。

本文将使用Mysql的驱动。
首先将下面的语句添加到你的源文件头部:

//如果没有该包:go get -u github.com/go-sql-driver/mysql
import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)

请注意,我们正在匿名加载数据库驱动程序,通过将其包限定符别名为_,这样在我们的代码中不会看到该驱动程序的导出的名称。 在底层,数据库驱动程序将执行init函数:

源文件:mysql.diver.go
func init() {
	sql.Register("mysql", &MySQLDriver{})
}

该init函数通过sql.Register方法调用,将自己注册到sql包中的drivers(该变量的底层数据机构是一个map[string]driver.Driver)变量中。

源文件:sql.sql.go
var (
	driversMu sync.RWMutex
	drivers   = make(map[string]driver.Driver)
)
func Register(name string, driver driver.Driver) {
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
	drivers[name] = driver
}

现在您已准备好访问数据库。

3.访问数据库

您已经加载了数据库驱动程序包,现在可以创建一个数据库对象,即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函数的首个参数是驱动的名称。这是数据库驱动程序用于向database/sql注册自身时候使用的字符串,通常与包名相同,以避免混淆。比如我们所使用的go-sql-driver驱动,当其想database/sql注册时使用的是“mysql”
  2. 第二个参数是一个特定于数据库驱动程序的语法,它告诉驱动程序如何访问底层数据存储。在本例中,我们连接到本地MySQL服务器实例中的“hello”数据库。
  3. 您应该(几乎)总是检查和处理database/sql操作所返回的错误。有一些特殊的情况我们将在后面讨论,在这些情况下这样做是没有意义的。
  4. 如果sql.DB的声明周期不应超出函数的作用域,那么defer db.Close()延迟语句是惯用做法

也许与直觉相反,sql.Open()并不建立到底层数据库的任何连接,也不会验证数据库驱动程序的连接参数(即参数二)。相反,它只是为以后的使用准备好了一个数据库抽象。当第一次需要时,它将惰性地建立到底层数据存储的第一个实际连接。如果您想立即检查数据库是否可用和可访问(例如,检查您是否可以建立网络连接并登录),请使用db.Ping()来完成这项工作,但别忘了检查错误哦:

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

虽然在使用完数据库之后执行Close()是一种习惯用法,但是sql.DB对象被设计为长期【long-lived】的。不要频繁的Open()和Close()数据库。相反,为您所需要访问的每个不同数据存储创建一个sql.DB对象,并将其保留到程序完成对该数据存储的访问为止。根据需要传递它,或者使它以某种方式在全局可用,但是要保持它没有被关闭。不要从短期【short-lived】函数中Open()和Close()数据库,而是将sql.DB作为参数传递给该short-lived函数。

如果没有按照database/sql设计的意图把sql.DB当成一个长期【long-lived】对象来用,那么您可能会遇到一些问题,比如连接的重用性和共享性不佳、可用网络资源耗尽,或者由于大量TCP连接仍然处于TIME_WAIT状态而导致偶发故障。

现在是时候使用sq.DB对象了。

4. 检索结果集

从数据存储中检索结果有几个惯用操作:

  1. 执行查询【query】,返回查询所得的行【rows】。
  2. 准备语句【statement】以多次重用,我们可以多次调用它,然后再销毁它。
  3. 以一次性的方式执行语句【statement】,而不为重复使用做准备。
  4. 执行返回一行【row】的查询【query 】

Go的database/sql中函数的命名非常具有意义。如果函数名中包含Query,则它旨在向数据库提出问题,并返回一组行【rows】,即使它是空的。没有行【row】返回的语句,不可以使用Query函数,它们应该使用Exec().

4.1 Fetching Data from the Database

让我们看一个示例,该示例中演示了如何查询数据库并处理结果。我们将查询user表中id = 1的用户,并打印出该用户的id和name。我们将使用rows.Scan()将结果分配给变量,一次处理一行【row】。

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() 将查询发送到数据库,并像往常一样检查error 。
  2. 我们使用defer延迟了rows.Close(),这一点非常重要
  3. 我们使用rows.Next()作为循环条件,来迭代数据库返回的结果集Rows
  4. 我们使用rows.Scan(),读取结果集Rows中的一行,并将该行中的每一列的值读入变量中
  5. 我们在迭代遍历完结果集Rows之后检查error 。

这几乎是Go中操作数据库的唯一方式了。例如,您无法将行【row】作为map来获取。这是因为所有东西都是强类型的。所以用户需要创建相应类型的变量,并在rows.Scan中传入其指针,Scan函数会根据目标变量的类型执行相应转换,如代码中所示。

其中有几个部分很容易出错,并可能产生不好的后果。

  • 您应该在for rows.Next()循环结束后,通过rows.Err()方法检查迭代过程中是否有error 。如果在循环过程中有err,您需要了解它。不要仅仅认为循环会迭代,直到处理完所有的结果集Rows。
  • 其次,只要结果集是打开的(即我们示例中的rows),底层连接就处于占用【Busy】状态,而不能用于任何其他查询。这意味着它在连接池中不可用。如果您使用rows. next()遍历所有行,最终您将读取到最后一行时,rows. next()将遇到一个内部EOF错误,并为您调用rows. close()。但是,如果由于某种原因退出了该循环(提前返回,等等),那么该结果集Rows就不会被关闭,底层的连接仍然是打开的(但是,如果row.next()由于错误返回了false,那么它将自动关闭)。这是一种很容易耗尽资源的方法。
  • close()是一个无害的空操作【no-op】,即使它已经关闭,因此您可以多次调用它。但是,请注意,我们首先检查error ,并且只在没有error 的情况下才调用rows.Close(),以避免运行时恐慌。
  • 您应该总是使用defer延迟对row.close()的执行,即使您也在循环结束时显式地调用了rows.Close(),这也不是一个坏主意。
  • 不要在循环中使用defer。defer语句直到函数退出时才会执行,所以长时间运行的函数不应该使用它。如果你这样做,你会慢慢累积内存。如果您在循环中反复查询和使用结果集,那么当您处理完每个结果时,应该显式地调用rows.Close(),而不是使用defer。

4.2 Scan()工作原理

当您对结果集中的每一行进行迭代,并将它们扫描到目标变量中时,Go会基于目标变量的类型,在幕后为您执行数据类型转换工作。意识到这一点可以整洁您的代码,并帮助避免重复的工作。

例如,假设您从一个由字符串类型的列【column】定义的表(例如VARCHAR(45)或类似的表)中查询筛选出结果集。然而,您碰巧知道这个表中的值总是包含数字的字符串(例如:“4”)。如果您传递一个指向字符串的指针,Go会将把字节复制到字符串中。现在您就可以使用strconv.ParseInt()或类似的方法将该值转换为数字。但是您必须检查SQL操作中的error,以及解析整数时的error,而这是混乱和乏味的。

或者,您可以向Scan()传递一个指向Integer类型变量的指针。Go将检测到这一点,并为您调用strconv.ParseInt()。如果转换中出现错误,Scan()将返回相应的错误。您的代码现在更整洁、更精简了。这就是在Go中使用database/sql的标准方式

4.3 准备查询

通常,您应该准备查询语句,以用于多次查询使用。准备查询的结果是产生一个预置语句/预处理语句【prepared statement】,它可以为您在执行语句时提供的参数使用占位符(即绑定值)。这比串联字符串安全多了,原因很多(比多可以避免SQL注入)。

在MySQL中,参数占位符是?,在PostgreSQL中是$N,其中N是一个数字。SQLite支持这两种方法。在Oracle中,占位符以冒号开头,紧跟着一个命名,如:param1。因为我们使用的是Mysql,所以我们使用?

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()也会prepare、execute和close一个预置语句/预处理语句【prepared statement】。这是到数据库的三次往返。如果您不小心,您可能会将应用程序进行的数据库交互的数量增加三倍!一些驱动程序可以在特定的情况下避免这种情况,但不是所有的驱动程序都这样做。有关更多信息,请参见第六节。

4.4 单行查询

如果查询最多返回一行,可以使用快捷方式绕过一些冗长的样板代码:

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

查询中的Errors将被延迟,直到Scan()被调用,然后从中返回。您亦可以在预置语句/预处理语句【prepared statement】上调用QueryRow():

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

5 修改数据和使用事物

现在,我们准备来了解如何修改数据并处理事务。如果您已经习惯了使用“Statement”对象来获取查询结果集Rows和更新数据的编程语言,那么这种区别似乎是人为的,但是在Go中,这种区别有一个重要的原因。

5.1 修改数据的语句

使用Exec(),最好与预置语句/预处理语句【prepared statement】一起使用,来完成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)

执行该语句将生成一个sql.Result,我们可以通过它来访问语句元数据:最后插入的ID和受影响的行数。

如果你不在乎结果呢?如果您只想执行一条语句并检查是否有错误,但是忽略结果,该怎么办?下面这两个语句会得到同样的效果吗?

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

答案是否定的。您永远不应该像这样使用Query()。Query()将返回一个sql.Rows,它将一直保持数据库连接,直到sql.Rows执行close()。因为可能有未读数据(例如更多的数据行),所以该底层数据库连接无法被使用。在上面的例子中,底层数据库连接将永远不会被释放。最终垃圾回收器会帮您关闭掉底层的net.Conn,但这可能需要很长时间。此外,database/sql 包会对其连接池中的连接保持追踪,希望您在某个时候释放它,以便连接可以再次使用。因此,此反模式是耗尽资源(例如,连接太多)的好方法。

5.2 事物

在Go中,事务本质上是一个持有到底层数据存储的连接的对象。它允许您执行到目前为止我们所看到的所有操作,但是保证它们将在相同的数据库连接上执行。

您可以通过调用db.Begin()来开启一个事物,然后在该函数返回的Tx对象上执行Commit()或者Rollback()方法来结束一个事物。实际上,Tx会从连接池中获取一个连接,并持有它仅用于该事务。在Tx上可以应用的方法与可以再数据库上调用的方法一一对应,如Query()等。

事务对象也可以准备(prepare)语句,由事务创建的预置语句/预处理语句【prepared statement】会显式绑定到创建它的事务。有关更多信息,请参见第六节。

使用事务时,您不应该将诸如Begin()和Commit()等事务相关函数,与执行事务相关的SQL语句,例如BEGIN,COMMIT等混合使用。这可能产生一些副作用:

  • Tx对象会保持打开状态,占用连接池中的数据库连接,而不释放它
  • 数据库状态不再与Go中相关变量的状态保持同步。
  • 您可能认为您正在事务内部的单个数据库连接上执行查询,而实际上Go无形之中为您创建了多个数据库连接,而某些语句不是事务的一部分。

当处于事务内部时,您应该小心不要调用Db变量。而是应该讲所有的调用应用到由db.Begin()创建的Tx变量上。Db并不是事务的一部分,只有Tx才是。如果您在事物中进一步的调用了db.Exec()或类似的方法,那么这些调用将发生在您的事务范围之外的其他连接上。

如果需要处理修改连接状态的多个语句,也需要用到Tx对象,即使您本身不想要事务。例如:

  • 创建仅连接可见的临时表
  • 设置变量,例如MySQL的SET @var:= somevalue语法。
  • 更改连接选项,例如字符集,超时设置。

如果你需要做这些事情中的任何一件,你需要将你的活动绑定到一个单独的连接,在Go中唯一的方法就是使用一个Tx。在Tx上执行的方法调用都可以保证在同一个底层数据库连接上执行,这使得对连接状态的修改对后续操作起效。

6 使用预置语句/预处理语句【prepared statement】

在Go中,预置语句/预处理语句【prepared statement】具有所有常见的优点:安全性、效率和方便性。但是它们的实现方式与您可能习惯的方式稍有不同,特别是在它们如何与数据库/sql的一些内部元素交互方面。

6.1 预置语句/预处理语句【prepared statement】和连接【connection】

在数据库级别,预置语句/预处理语句【prepared statement】被绑定到单个数据库连接。典型的流程是:客户端将带有占位符的SQL语句发送到服务器进行预备,服务器以一个语句ID进行响应,然后客户机通过发送其语句ID和参数来执行语句。
然而,在Go中,连接不会直接暴露给database/sql包的用户。因此您无法在数据库连接上准备语句,但是您可以在Bc或Tx上进行准备,database/sql具有一些方便的行为,例如自动重试。由于这些原因,预置语句/预处理语句【prepared statement】和存在于驱动级别的连接之间的底层关联对代码是隐藏的。
它是这样工作的:

  • 当您准备一条语句时,它是在数据库连接池中的某一连接上准备的。
  • Stmt对象记住使用了哪个数据库连接。
  • 当您执行Stmt时,它会尝试使用该数据库连接。如果它因为关闭或忙于做其他事情而不可用,则从数据库连接池中获取另一个连接,然后在连接上与数据库互动以重新准备语句。

因为当语句的原始连接繁忙或关闭时,将根据需要重新准备语句,所以在数据库高并发情况下,这是可能发生的。这可能导致明显的语句泄漏,比您想象的更频繁地准备和重新准备语句,甚至会导致服务器端对语句数量的限制。

当语句【statement】的原始连接处于繁忙或者关闭状态时,将根据需要重新准备语句,这在数据库高并发时很可能会出现,因为高并发情况下大多数连接处于繁忙状态,因此大量的预置语句/预处理语句【prepared statement】需要重新准备。这可能导致明显的语句泄漏,比您想象中更频繁的准备和重新准备语句,甚至会导致服务器端对语句数量的限制。

6.2 避免预置语句/预处理语句【prepared statement】

Go在幕后为您创建预置语句/预处理语句【prepared statement】。例如简单的 db.Query(sql, param1, param2),他会先准备语句,然后使用参数执行它,最后关闭语句。

然而,有时预置语句/预处理语句【prepared statement】并不是您想要的。这可能有几个原因:

  1. 数据库不支持预置语句。例如,t通过使用MySQL驱动,您可以连接到MemSQL和Sphinx,因为它们都支持MySQL连接协议。但是它们不支持包含预置语句/预处理语句【prepared statement】在内的的“binary”协议,因此它们可能以令人困惑的方式失败。
  2. 这些语句没有得到足够的重用以使它们有价值,并且安全性问题以其他方式处理,因此不希望出现性能开销。这方面的一个例子可以在VividCortex blog上查阅。

如果不想使用预置语句/预处理语句【prepared statement】,则需要使用fmt.Sprint()或类似的方法来组装SQL,并将其作为惟一的参数传递给db.Query()或db.QueryRow()。你的驱动程序需要支持明文查询执行,而这是在Go1.1时通过Execer 和Queryer引入的,Click:文档连接

6.3 事物中预置语句/预处理语句【prepared statement】

在Tx中创建的预置语句/预处理语句【prepared statement】只绑定到该事物,因此前面关于重新准备的警告不适用于此操作。当您操作一个Tx对象时,您的操作将直接映射到它下面的一个且仅一个数据库连接上。

这也意味着在Tx中创建的预置语句/预处理语句【prepared statement】不能与它分开使用。同样,在DB上创建的预置语句也不能在事务中使用,因为它们将绑定到不同的连接。

要在Tx中使用在事务外部的预置语句/预处理语句【prepared statement】,您可以使用Tx. stmt(),它将从事务外部的预置语句/预处理语句【prepared statement】创建一个新的特定于事务的语句。它获取现有的预置语句/预处理语句【prepared statement】,将该语句的连接设置为事物的连接,每次执行语句时都要准备它们。这种行为及其实现是不可取的,甚至在database/sql源代码中有一个TODO标识我们未来会改进它;但是我们建议不要使用它。

在处理事务中的预置语句/预处理语句【prepared statement】时必须谨慎。考虑下面的例子:

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!

在Go 1.4之前的版本中,关闭*sql.Tx会将与之关联的连接释放回连接池中,但是,使用defer对预置语句/预处理语句【prepared statement】执行延迟关闭是发生在这之后的,而这可能会导致对底层连接的并发访问,从而导致连接状态不一致。如果使用Go 1.4或更高版本,应该确保在提交或回滚事务之前始终关闭语句【statement】。CR 131650043在Go 1.4中修复了这个问题.

6.4 参数占位符

预置语句/预处理语句【prepared statement】中占位符参数的语法是特定于数据库的。下面我们来对MySQL、PostgreSQL和Oracle三者进行比较:

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

7 异常处理

几乎所有使用database/sql类型的操作都会返回一个error作为最后一个值。你应该经常检查这些错误,永远不要忽略它们。
但是有一些比较特殊的error,需要我们注意:

7.1 Errors From Iterating Resultsets

考虑下面的代码:

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

从rows.Err()函数返回的Error可能是rows.Next()循环中各种错误的结果。循环可能由于某些原因而退出,而不是正常完成循环,因此您总是需要检查循环是否正常终止。异常终止会自动调用rows.Close(),尽管多次调用它是无害的。

7.2 Errors From Closing Resultsets

如前所述,如果过早地退出循环,那么您应该始终显式地关闭sql.Rows。如果循环正常或出现错误,它会自动关闭,但您可能会错误地这样做:

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()返回的Error可能是唯一的例外。如果rows.Close()返回了一个Error的话,不清楚应该做什么。对错误消息或恐慌日志记录可能是唯一明智的做法,如果这是不合理的,那么也许您应该忽略这个错误。

7.3 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()返回的就是sql.ErrNoRows。在大多数情况下,这需要作为特殊情况来处理。空结果通常不会被应用程序代码视为错误,如果你不检查返回的Errro是否是这个特殊的常量,这将导致您的应用程序以您所不希望的方式失败。

查询中的Error将被延迟到调用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)

您可能想问,问什么空的结果被视为了Error。空集并没有错。原因是QueryRow()方法需要使用这种特殊情况,以便让调用者区分QueryRow()是否确实找到了一行;如果没有它,Scan()不会执行任何操作,而且您可能没有意识到您的变量根本没有从数据库中获得任何值。

只有在使用QueryRow()时才会遇到此错误。如果您在其他地方遇到这个错误,说明您做错了什么。

7.4 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的数值码来确定Error.
但是,实现这一点的机制因驱动程序而异,因为这不是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字段在各个驱动程序之间可能有所不同。然而,这个数字的值是从MySQL的错误消息中提取的,因此是特定于数据库的,而不是特定于驱动程序的。

这段代码仍然很难看。与1045(一个神奇的数字)相比,1045是一种代码气味。一些驱动程序提供了一个错误标识符列表。例如,Postgres pq驱动程序就是这么做的,在error.go中。有一个由VividCortex维护得MySQL错误号的外部包。使用这样的列表,上面的代码写得更好:

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

7.5 Handling Connection Errors

如果您到数据库的连接被删除、终止或出现错误怎么办?
发生这种情况时,不需要实现任何逻辑来重试失败的语句。作为database/sql中的连接池的一部分,内置了处理连接失败的功能。如果您执行一个查询或其他语句,而底层连接失败,Go将重新打开一个新连接(或从连接池中获取另一个连接)并重试,最多10次。
然而,可能会有一些意想不到的后果。当发生其他错误条件时,可以重试某些类型的错误。这也可能是特定于驱动程序的。MySQL驱动程序的一个例子是,使用KILL取消一个不需要的语句(例如长时间运行的查询)会导致语句被重试10次.

8.Working with NULLs

列可空很烦人,会导致很多丑陋的代码。如果可以的话,避免这样做。如果没有这样做,则需要使用database/sql包中的特殊类型来处理它们,或者定义自己的类型。
有可空的布尔值、字符串、整数和浮点数的类型。你可以这样使用它们:

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

可空类型的限制,以及在需要更有说服力的情况下避免使用可空列的原因:

  • 没有sql.NullUint64或sql.NullYourFavoriteType。你需要为这个定义你自己的。
  • 可空性是很棘手的,而且未来可能会过时。如果您认为某些东西不会为空,但是您错了,那么您的程序将会崩溃,这种数据可能很少,以至于你无法在发布前得到。
  • 关于Go的一个好处是为每个变量提供了一个有用的默认零值。这不是可以为空的东西工作的方式。

如果需要定义自己的类型来处理null,可以复制sql.NullString来实现这一点。
如果不能避免数据库中有NULL值,多数数据库系统都支持另一项工作方式,即COALESCE()。您可以使用以下内容,而不需要引入大量sql.Null*类型。

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

9. Working with Unknown Columns

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

如果不知道查询将返回多少列,可以使用Columns()查找列名列表。您可以检查这个列表的长度,以查看有多少列,并且可以将一个适合长度的切片传递给Scan():

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

10.The Connection Pool

在database/sql包中有一个基本的连接池。我们没有太多的能力来控制或检查它,但以下是一些你可能会发现有用的知识:

  • 连接池【Connection Pool】意味着在单个数据库上执行两个连续语句可能会打开两个连接并分别执行它们。程序员很容易搞不清楚为什么他们的代码行为不正常。例如,紧跟在INSERT后的LOCK TABLES会阻塞,因为INSERT位于一个不保存表锁的连接上。
  • 当需要数据库连接,并且连接池中没有空闲的连接时,就会创建连接
  • 默认情况下,连接数没有限制。如果您尝试同时执行大量操作,则可以创建任意数量的连接。这可能导致数据库返回错误,例如“too many connections”。
  • 在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)来指定可以重用连接的最长时间,因为重用长期的连接可能会导致网络问题。通过该设置,将延迟关闭未使用的连接,即关闭过期的连接。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值