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.mod是go moudle管理包的工具,是GO 1.11之后推出的,并且从GO 1.13之后是GO默认的依赖管理工具。main.go是程序的入口,main函数的所在的文件pg_manager.gopg连接池代码实现的文件
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 channel的select会一直被阻塞。
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做一个简单的入门介绍。