关于Go的database.sql.DB的使用

PS:本文是看到一个很好的教学网页,觉得很好,下面的内容基本都是直接翻译的。
网站链接:http://go-database-sql.org/index.html

官方的database.sql包中提供了好用的数据库操作接口,但是这些接口需要相应的方法才能高效的利用否则可能导致问题。

sql.DB是数据库操作的关键抽象对象,提供sql查询、结果返回、事务操作、连接开闭、连接池维护等功能。

导入具体数据库驱动的方法

1
2
3
4
import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)

有些数据库鼓励你去直接使用包中的函数,但这是很不明智的,使用上面的方法可以引入驱动,但不直接使用,而是利用sql.DB对象进行操作,这样更符合Go的设计、使用效率更高,针对接口编程兼容性也更好。

创建DB对象

1
2
3
4
5
6
7
8
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()函数其实并没有真正创建数据库连接,而是推迟到进行数据库操作时才连接。所以为了验证数据库是否有效,可以用下面的语句:

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

注意,上面代码中defer db.Close()是我们常用的方法,及时关闭数据库连接。但是在使用sql.DB对象时,应该尽量延长对象生存周期,通过参数传递、全局作用域等方式利用DB对象。如果频繁创建、关闭DB对象,会发生一系列网络、数据库问题,因为这不是正确使用DB对象的方法。DB对象本身会在后台维护一个连接池,具体细节我们就不用管了。

sql.Query()会返回rows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}

注意要在结束rows的迭代后检查rows.Err(),并且要及时调用rows.Close()释放连接给连接池,在rows使用的过程中连接是一直被占用的。

可以利用db.Prepare进行带参数的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}

※Prepare也可能会造成连接资源占用的问题,在实际使用中要考虑到。

在不关心SQL执行结果的情况下,最好用db.Exec()函数

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

上面对Query的使用是错误的,返回的rows会一直得不到释放,虽然最终会被垃圾回收机制释放,但是会耗费很多时间、占用了资源。

由于数据库的各种操作和连接都可能产生错误,所以应该在每一次操作都尽量查看是否存在错误,避免程序错误执行。注意进行错误码比较时不要用魔法数字,要用常量。

可置NULL的字段的处理方式:

1
2
3
4
5
6
7
8
9
10
for rows.Next() {
	var s sql.NullString
	err := rows.Scan(&s)
	// check err
	if s.Valid {
	   // use s.String
	} else {
	   // NULL value
	}
}

在执行事务操作时,当前的连接是处于保持连接并准备执行事务状态的,所以这个过程如果执行其他不相关的sql语句,本质上是其他新的连接执行的,这点要意识到。

对返回的rows的字段数量和类型未知的情况下,可以用sql.RawBytes

1
2
3
4
5
6
7
8
9
10
11
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.
}

关于连接池,可以利用一系列参数设置函数,针对自己的业务进行调参优化。

1
2
3
db.SetMaxIdleConns(N)
db.SetMaxOpenConns(N)
db.SetConnMaxLifetime(duration)

目前对存储过程(Stored Procedures)的调用支持的不太好,调用后无法返回正确的结果。(这个可能得直接调用驱动包中的函数)