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

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

访问数据库

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

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"。于是把这段代码封装成一个公共方法供其他地方调用:

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了,不过事情可没这么简单。

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

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

连接池问题

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

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

现在再回过头来看代码,既然open创建了连接池,那用完把它销毁不就好了,于是参考官网文档稍加改进:

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

在Golang中如何正确地使用database/sql包访问数据库的更多相关文章

  1. Golang中如何正确的使用sarama包操作Kafka?

    Golang中如何正确的使用sarama包操作Kafka? 一.背景 在一些业务系统中,模块之间通过引入Kafka解藕,拿IM举例(图来源): 用户A给B发送消息,msg_gateway收到消息后,投 ...

  2. golang——database/sql包学习

    1.database/sql包 sql包提供了保证SQL或类SQL数据库的泛用接口. 使用sql包时必须注入(至少)一个数据库驱动. (1)获取mysql driver:go get -v githu ...

  3. 使用Spring.net中对Ado.net的抽象封装来访问数据库

    使用Spring.net中对Ado.net的抽象封装来访问数据库     Spring.NET是一个应用程序框架,其目的是协助开发人员创建企业级的.NET应用程序.它提供了很多方面的功能,比如依赖注入 ...

  4. 关于Golang中database/sql包的学习

    go-sql-driver 请求一个连接的函数有好几种,执行完毕处理连接的方式稍有差别,大致如下: db.Ping() 调用完毕后会马上把连接返回给连接池. db.Exec() 调用完毕后会马上把连接 ...

  5. Golang中database/sql包

    驱动 github.com/go-sql-driver/mysql 请求一个连接的函数有好几种,执行完毕处理连接的方式稍有差别,大致如下: db.Ping() 调用完毕后会马上把连接返回给连接池. d ...

  6. Golang中Struct与DB中表字段通过反射自动映射 - sqlmapper

    Golang中操作数据库已经有现成的库"database/sql"可以用,但是"database/sql"只提供了最基础的操作接口: 对数据库中一张表的增删改查 ...

  7. go标准库的学习-database/sql

    参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口. 使 ...

  8. go database/sql sql-driver/mysql 操作

    这里使用的是github.com/Go-SQL-Driver/MySQL, 所以需要下载一个github.com/Go-SQL-Driver/MySQL 引入 database/sql 和 githu ...

  9. golang中Context的使用场景

    golang中Context的使用场景 context在Go1.7之后就进入标准库中了.它主要的用处如果用一句话来说,是在于控制goroutine的生命周期.当一个计算任务被goroutine承接了之 ...

随机推荐

  1. .NET Core 3.0或3.1 类库项目中引用 Microsoft.AspNetCore.App

    本文为原创文章.首发:http://www.zyiz.net/ 在 ASP.NET Core 3.0+ web 项目中已经不需要在 .csproj 中添加对 Microsoft.AspNetCore. ...

  2. [MVC] - Asynchronous操作

    最近在项目里遇到个问题,是在MVC的项目里处理异步操作的,操作流程是这样的: 1.从MVC的一个controller调用一个异步方法: public ActionResult CreateOrUpda ...

  3. java中定时器设置时间

    <!-- 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 0 0 12 ? * WED 表示每个星期三中午1 ...

  4. 扫盲:Kotlin 的泛型

    引子 相信总是有很多同学,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼. Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin ...

  5. JavaSwing实现的文本比较软件

    先看效果:截图1 截图2: 实现思路: 1.界面UI设计 2.功能点 : a 打开文件进行比较    b 粘贴内容进去比较   c 提示帮助  d 窗口可以任意拖动 3.文本比较算法  java类 : ...

  6. 事务的概念,以及事务在JDBC编程中处理事务的步骤

    事务是作为单个逻辑工作单元执行的一系列操作,一个逻辑工作单元必须有四个属性,称为原子性.一致性.隔离性和持久性 (ACID) 属性,只有这样才能成为一个事务 .JDBC处理事务有如下操作: 1,con ...

  7. C#设计模式——建造者模式(Builder Pattern)

    1.建造者模式简介 1.1>.定义 建造者模式(Builder)将复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示. 1.2>.使用频率  中低 1.3>.原型模式应用 ...

  8. 迭代器设计模式,帮你大幅提升Python性能

    大家好,我们的git专题已经更新结束了,所以开始继续给大家写一点设计模式的内容. 今天给大家介绍的设计模式非常简单,叫做iterator,也就是迭代器模式.迭代器是Python语言当中一个非常重要的内 ...

  9. OpenSUSE 使用基础

    OpenSUSE OpenSUSE 是一个基于 RPM 的发行版,这和 RHEL/CentOS 一致. 但是它的官方包管理器是专有的 zypper,挺好用的,软件也很新. 本文以 OpenSUSE L ...

  10. Java基础--接口回调(接口 对象名 = new 类名)理解

    接口 对象名1 = new 类名和类名 对象名2 = new 类名的区别是什么? 实例 /** *Person.java 接口 */ public interface Person { void in ...