阅读 712

GO第一次实践:GO实现PG数据库连接池

1 前言

作为docker,k8s的重度使用用户,对开发它们的基础语言必须要熟悉。并且,创建k8s的crd无论使用kubebuilder还是operator-sdk,都是基于go语言之上运行的框架。之前使用过operator-sdk创建过限制执行固定次数的cronjob任务,对go也多少有了解,但是没有系统地学习过go,最近抽空从go支持的数据类型,函数,对象,并发性等方面进行了学习。

因此,从常用的数据库连接池应用上面来做一个简单的demo,针对用到的特性进行介绍。

2 demo

2.1 代码结构

[root@t32 web]# tree -L 2 .
.
├── go.mod
├── go.sum
├── main.go
└── pg_manager
    └── pg_manager.go
复制代码
  • go.modgo moudle管理包的工具,是GO 1.11之后推出的,并且从GO 1.13之后是GO默认的依赖管理工具。
  • main.go 是程序的入口,main函数的所在的文件
  • pg_manager.go pg连接池代码实现的文件

2.2 代码实现

(1) pg_manager.go

package pg_manager

import (
	"database/sql"
	"errors"
	"fmt"
	_ "github.com/lib/pq"
	"log"
)

type MyDB struct {
	pool chan *sql.DB
}

var ErrPoolClosed = errors.New("连接池已经关闭!")

const (
	host     = "192.168.4.31"
	port     = 5432
	user     = "postgres"
	password = "123456"
	dbname   = "airdb"
)

func (md *MyDB) New(size int) {
	md.pool = make(chan *sql.DB, size)
}

func (md *MyDB) Close() {

	close(md.pool)
	for db := range md.pool {
		db.Close()
	}
	log.Println("Close:", "资源回收成功!")

}

func (md *MyDB) createDBConn() (*sql.DB) {
	pgSqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
	db, err := sql.Open("postgres", pgSqlInfo)

	if err != nil {
		log.Println("DB Open:", err)
		return nil
	}
	err = db.Ping()
	if err != nil {
		log.Println("DB PING: ", err)
		return nil
	}
	return db
}

func (md *MyDB) getDBConn() (*sql.DB, error) {
	select {
	case r, ok := <-md.pool:
		log.Println("getDBConn:", "从连接池中获取资源...")
		if !ok {
			return nil, ErrPoolClosed
		}
		return r, nil
	default:
		log.Println("getDBConn:", "创建新的资源...")
		return md.createDBConn(), nil
	}
}

func (md *MyDB) putDBConn(db *sql.DB) {
	select {
	case md.pool <- db:
		log.Println("putDBConn", "连接放入池中...")
	default:
		log.Println("putDBConn", "队列已满,关闭当前连接...")
		db.Close()
	}
}

func (md *MyDB) ReadMany(readSql string) (r []string) {
	db, err := md.getDBConn()
	if err != nil {
		log.Println(err)
	}
	md.putDBConn(db)
	var result []string
	rows, err := db.Query(readSql)
	if err != nil {
		log.Println(err)
	}

	for rows.Next() {
		var image string
		err := rows.Scan(&image)
		if err != nil {
			log.Println("Read Many:", err)
		}
		result = append(result, image)
	}
	return result
}

func (md *MyDB) ReadOne(readSql string) (r string) {

	db, err := md.getDBConn()
	if err != nil {
		log.Println(err)
	}
	defer md.putDBConn(db)
	row := db.QueryRow(readSql, 1)

	var image string
	err = row.Scan(&image)
	if err != nil {
		log.Println("Read Many:", err)
	}
	return image
}

复制代码
  • 数据类型
    • type MyDB struct{}:定义结构体,可以理解为类
    • md *MyDB :指针,学习过C/C++的同学应该很好理解,指针保存对象的地址
    • pool chan *sql.DB: 管道,GO语言的美妙之处,通过channel可以便于并发单元之间进行传输数据。也可以作为FIFO的队列,在实现连接池过程中就把chan作为了一个队列。
    • var result []string: 切片,可以解决为python中的list,与之对应的是数据,数据是定长的,切片可以动态增加。
  • 关键字
    • const:常量
    • func:定义方法
    • defer: 在函数工作结束后执行一些收尾工作,在当前函数或者方法返回之前执行,可以完成一些关闭io,数据库回滚等操作。
    • select: select 是一种与 switch 相似的控制结构,与 switch 不同的是,select 中虽然也有多个 case,但是这些 case 中的表达式必须都是 Channel 的收发操作,如果case不满足,则执行default对应的操作。
  • 函数:以func (md *MyDB) ReadMany(readSql string) (r []string)为例
    • (md *MyDB): 表示这个方法属于MyDB结构体对应的对象
    • (readSql string): 表示入参
    • (r []string):表示返回结果

(2) main.go

package main

import (
	"fmt"
	pm "web/pg_manager"
)

var mydb pm.MyDB

func init() {

	mydb.New(20)
}

func main() {

	defer mydb.Close()

	sql := "select row_to_json(t.*) from (select * from imagetab where user_id=17) t"
	results := mydb.ReadMany(sql)
	fmt.Println(results)
}
复制代码
  • init():执行顺序在main函数之前。

3 重点讨论

3.1 go中的循环

go循环只有一种,那就是for循环,一共有三种使用方式

  • 固定的循环次数
for i:=0;i<10;i++{
    fmt.Println(i)
}
复制代码
  • 判断条件
i:=0
for i<10{
    fmt.Println(i)
    i++
}

// 无限循环
for {
    // do something
}
复制代码
  • range

range对字符串、数组、切片、管道等进行迭代输出元素

var a=[5]int{0,1,2,3,4}
for x:=range a{
    fmt.Println(x)
}
复制代码

3.2 go 的channel

go的channel语言相对比较简单,但是应用场景却十分广泛。

  • 无缓冲channel
var c chan int  // 声明
c:=make(chan int) // 创建无缓冲的管道
c<-1  // 管道中传入数据,必须有对应接受者,否则会阻塞
b,ok:=<-c //  从管道中获取数据,必须有对应的发送者,否则也会阻塞等待
复制代码
  • 有固定大小的channel
var c chan int  // 声明
c:=make(chan int,4) // 创建无缓冲的管道
c<-1  // 管道中传入数据,不需要有接收者,但是如果管道满了,会等待
b,ok:=<-c // 从管道中获取数据,如果管道为空,则等待
复制代码
  • 关闭管道
close(channel) // 管道关闭,不能再向管道中插入数据,但是可以获取,获取到最后一个值,ok为false
复制代码
  • 遍历channel
for x:= range chan{
    // do something
}
复制代码

range chan产生的迭代值为Channel中发送的值,它会一直迭代直到channel被关闭。

注:上面的例子中如果把close(md.pool)放在后面,程序会一直阻塞在for循环中那一行。

3.3 select

select语句选择一组可能的发送和接收操作去处理。它类似switch,但是只是用来处理channel操作。case可以是发送语句,也可以是接收语句,或者default

接收语句可以将值赋值给一个或者两个变量。它必须是一个接收操作。

最多允许有一个default case,它可以放在case列表的任何位置。

如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理。如果没有case需要处理,则会选择default去处理。如果没有default case,则select语句会阻塞,直到某个case需要处理。

需要注意的是,nil channel(var c chan int,没有make)上的操作会一直被阻塞,如果没有default case,只有nil channelselect会一直被阻塞。

select语句和switch语句一样,它不是循环,它只会选择一个case来处理,如果想一直处理channel,你可以在外面加一个无限的for循环:

for {
    select {
    case r,ok:=<-chan:
      // do something
    default:
      // do something 
    }
}
复制代码

4 总结

这只能算是go的一篇入门学习,有关go更多的性能,包括锁,继承,并发等性能,等理解的更加深入的时候再做总结,以免误导大家。

感谢@eudore指正,sql.DB的确本身就是数据库连接池,在database/sql源码下DB结构体定义的freeConn []*driverConn,driverConn为真实的数据库连接。不过并不影响这篇博文想表达的意图,即用这个demo来对go做一个简单的入门介绍。

文章分类
后端
文章标签