在Golang中如何正确地使用database/sql包访问数据库

本文记录了我在实际工做中关于数据库操做上一些小经验,也是新手入门golang时我认为必定会碰到问题,没有什么高大上的东西,因此但愿能抛砖引玉,也算是对这个问题的一次总结。html

其实我也是一个新手,机缘巧合几个月前开始作golang开发,之前一直是以.NET技术栈为主,文章若有错误不吝指正。java

访问数据库

相信你们第一次碰到这个问题的时候应该和我同样,去网上找个例子参考一下。没错,这样的例子一搜一大把,因而咱们很容易(抄)写了以下一段代码:mysql

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

func main() {
	db, err := sql.Open("mysql","root:111111@tcp(127.0.0.1:3306)/testdb")
	if err != nil {
		panic(err)
	}
	err = db.Ping()
    if err != nil {
	    panic(err)
    }
    fmt.Println("Successfully connected!")
}

把程序运行起来一看,成功地输出了想看到的东西,心里一阵暗喜“so easy"。因而把这段代码封装成一个公共方法供其余地方调用:git

func GetDbContext() *sql.DB {
    db, err := sql.Open("mysql","root:111111@tcp(127.0.0.1:3306)/testdb")
	if err != nil {
		panic(err)
	}
	err = db.Ping()
    if err != nil {
	    panic(err)
    }
    return db
}

func DoSomething(){
    db := GetDbContext()
    rows,_ := db.Query("select * from table1")
}

没错我最先就是这么干的,而后开始愉快地转头写CRUD了,不过事情可没这么简单。github

很快, 编码五分钟捉虫两小时开场了。golang

慢慢的我就发现,在连续屡次操做数据库后就偶尔发生程序卡死的状况,请求一直是pending状态,只能杀死进程重启才能够。刚开始没在乎,也没有怀疑是数据库操做有问题,但后来愈来愈频繁严重影响到程序开发,没办法就记log加断点调试看是哪里出了问题。通过反复验证后肯定问题就出在执行SQL语句这里,这下懵了,我看网上你们都是这么写的怎么会有问题??sql

链接池问题

根据多年开发经验,大胆猜想SQL执行失败最大的可能性就是数据库链接不上,在确认数据库没有崩掉的状况下开始研究代码哪里写的不对,可是先后也就那么几行代码实在看不出什么毛病,只能开始深刻了研究database/sql包的知识点。数据库

经过查资料发现open完数据库后的返回对象sql.DB其实是一个链接池对象,并非单纯的某一个链接。它是一个抽象的数据访问接口,和数据库类型无关,固然也就和具体的数据库Schema无关。咱们要实现某一个数据库的访问单纯用这个包是不够的,还要引入具体的数据库驱动包,这个驱动才是真正实现数据库访问的东西。less

如今再回过头来看代码,既然open建立了链接池,那用完把它销毁不就行了,因而参考官网文档稍加改进:tcp

func GetDbContext() *sql.DB {
    db, err := sql.Open("mysql","root:111111@tcp(127.0.0.1:3306)/testdb")
	if err != nil {
		panic(err)
	}
	err = db.Ping()
    if err != nil {
	    panic(err)
    }
    return db
}

func DoSomething(){
    db := GetDbContext()
    defer db.Close()
    rows,_ := db.Query("select * from table1")
}

看似行得通,可是估计没人愿意这样作。缘由很明显,别的先不谈,建立和销毁链接池开销太大了,你这样对它于心何忍,拿着屠龙刀去砍柴。

使用链接池的好处就是不须要开发者频繁地建立和销毁链接,这两项工做都交给了链接池去作,咱们只须要在使用前找它要一个可用的链接,用完还回去就能够了。

这里引用一段官方文档中的原话:

Although it’s idiomatic to Close() the database when you’re finished with it, the sql.DB object is designed to be long-lived. Don’t Open() and Close() databases frequently. Instead, create one sql.DB object for each distinct datastore you need to access, and keep it until the program is done accessing that datastore. Pass it around as needed, or make it available somehow globally, but keep it open. And don’t Open() and Close() from a short-lived function. Instead, pass the sql.DB into that short-lived function as an argument.

核心意思就是sql.DB是一个长生命周期对象,你不要随便打开和关闭,而且建议你在程序中为每个数据库建立惟一的sql.DB

那么如今的问题就是如何保证程序中只有一个链接池呢?

很简单,使用一个全局变量便可,有点相似C#和java中static的味道,在Golang中可使用以下方法声明一个全局对象:

package demo

import (
	"database/sql"
)

var mydb,_ =  sql.Open("mysql","connection_string")

不过咱们的业务场景比较特殊,系统中有不少个数据库,要根据不一样参数去连不一样数据库,那么上面这种声明赋值方式就不行了,我稍加改进,结合map实现了链接池动态管理:

var envdbMap map[string]*sql.DB

func GetEnvDbContext(connector config.DbConnector) *sql.DB {
	if envdbMap == nil {
		envdbMap = make(map[string]*sql.DB)
	}

	db, ok := envdbMap[connector.ID]
	if ok {
		return db
	} else {
		connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", connector.Host, connector.Port, connector.UserName, connector.Password, connector.DatabaseName)
		db, err := sql.Open("postgres", connStr)

		envdbMap[connector.ID] = db
		return db
	}
}

原理很简单,就是用map装池子,池子装链接。

有借有还

到这里链接池已经准备好了,那么如何从池子中取一个可用的链接呢?这点池子已经帮你们考虑的很周到了,你们不须要写额外代码去获取链接,直接拿起池子用就能够了,内部会有一系列机制帮你弄到一个链接去执行SQL,之后有机会对池子的原理来作个解析。

可是用完要记得还回去,这个必须你手动去作,例如:

rows,_ := db.Query("select * from table1")
defer rows.Close()
// do sth...

最好不要在do sth以后作Close,由于一旦你这个过程当中发生异常,致使后面的Close没法执行,那么这个链接就一直被占用,日积月累TCP链接就被你耗光了

官方文档说了,rows.Close()是一种无害(harmless)操做,你能够作屡次,可是不能忘了作。

这里有个特殊状况要注意,对于那种没有返回结果的SQL语句,千万不要使用Query方法去执行,这会致使没法回收链接,这时候推荐使用Exec方法去执行。

配置链接池

默认状况下链接池没有数量限制,可是咱们的机器有TCP的数量限制,不要由于一个程序拖死一台机器,因此不推荐无限量的去使用。database/sql包提供了几个链接池配置参数,主要包含:

  • db.SetMaxIdleConns(N) 设置空闲链接的数量
  • db.SetMaxOpenConns(N) 设置打开的链接数量
  • db.SetConnMaxLifetime(duration) 设置链接的生存时间
    详细的介绍你们能够参考官方文档。

总结

通过以上分析,能够清晰的知道最开始的bug就是由于错误地使用了链接池致使数据库链接被耗光从而没法执行SQL语句,其实说简单也很简单。

以上就是工做中使用golang访问数据库的踩坑历程,但愿能帮到新接触golang的朋友,若有错误的地方欢迎指出,以避免误导他人。

参考链接

http://go-database-sql.org/accessing.html

http://go-database-sql.org/retrieving.html

http://go-database-sql.org/connection-pool.html

相关文章
相关标签/搜索