Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org
或者网站http://www.flysnow.org/ ,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。
在Go1.7之前,Go标准库还没有内置Context的时候,如果我们想在一个Http.Request里附加值,怎么做呢?一般都是Map对象,存储对应的Request以及附加的值,然后在需要的时候取出来,今天我们介绍的这个就是实现了一个类似于这样功能的库,因为比较简单,而且实用,所以就先选择它来分析。
安装
要使用这个库,需要先安装,在Go里,任何库的安装都是一样的,那就是通过go get。
1
$ go get github.com/gorilla/context
copy
安装之后,我们就可以使用了,下面来看一个存储数据,取出数据的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
import (
"github.com/gorilla/context"
"net/http"
"strconv"
)
func main () {
//启动一个Web服务
http.Handle ("/" ,http.HandlerFunc (myHander))
http.ListenAndServe (":1234" ,nil )
}
//定义一个Hander
func myHander (rw http.ResponseWriter, r * http.Request) {
//模拟为Request附加值,这里附加了2个
context.Set (r,"user" ,"张三" )
context.Set (r,"age" ,18 )
//这个模拟一个方法或者函数的调用,大部分情况下可能不在一个包里
doHander (rw ,r)
}
func doHander (rw http.ResponseWriter, r * http.Request) {
//我们从这个Request里取出对应的值。
user:= context.Get (r,"user" ).(string )
age:= context.Get (r,"age" ).(int )
rw.WriteHeader (http.StatusOK)
rw.Write ([]byte ("the user is " + user+ ",age is " + strconv.Itoa (age)))
}
copy
数据如何存储
上面是一个很简单的示例,用过context.Set
和context.Get
函数为一个*http.Request
附加我们想存储的键值对,在需要的时候取出他们。这样不管我们的*http.Request
被传递到哪里去,都可以获得我们存储的值,为我们为值得传递提供了便利。
1
2
3
4
5
6
7
8
9
func Set (r * http.Request, key, val interface {}) {
mutex.Lock ()
if data[r] == nil {
data[r] = make (map [interface {}]interface {})
datat[r] = time.Now ().Unix ()
}
data[r][key] = val
mutex.Unlock ()
}
copy
Set函数接受三个参数,第一个是*http.Request
,第二个是Key,第三个是值,从这三个参数看,我们可以为一个*http.Request
存储多个值。
值存储在什么地方呢?context库里使用的是map对象里。
1
2
3
var (
data = make (map [* http.Request]map [interface {}]interface {})
)
copy
data定义的是一个双层嵌套的map,第一层Key为*http.Request
,Value为map[interface{}]interface{}
;第二层的map的key和value都是interface{}
,意味着我们可以存储任何职。
在上面的Set函数中,先通过data[r] == nil
判断该Request对应的存储数据的map是否存在,如果没有的话,先创建该map:
1
data[r] = make (map [interface {}]interface {})
copy
创建好了之后,就可以为这个map附加值了。通过data[r][key] = val
进行赋值,就完成了一次存储。这里使用的是map,如果存储的时候,已经有了旧值,旧的值会被新的覆盖。
获取存储的值
现在我们知道,存储值得对象其实是个map,所以获取存储的值也比较简单了,像操作map一样获取值即可,现在看下context为我们提供的获取值得函数代码。
1
2
3
4
5
6
7
8
9
10
func Get (r * http.Request, key interface {}) interface {} {
mutex.RLock ()
if ctx := data[r]; ctx != nil {
value := ctx[key]
mutex.RUnlock ()
return value
}
mutex.RUnlock ()
return nil
}
copy
源代码逻辑上做了一些判断,存在就返回值,不存在就返回nil。
判断存储的Key是否存在
有时候我们需要判断我们存储的Key是否存在,但是我们不能通过返回的值是nil来判断,因为我们也可以为一个key设置一个nil的值,这是可行的,并且是存在的,所以我们不能使用这种方式来判断。
context库为我们特意提供了GetOk
函数来判断一个Key是否存在。
1
2
3
4
5
6
7
8
9
10
func GetOk (r * http.Request, key interface {}) (interface {}, bool ) {
mutex.RLock ()
if _, ok := data[r]; ok {
value, ok := data[r][key]
mutex.RUnlock ()
return value, ok
}
mutex.RUnlock ()
return nil , false
}
copy
从上面的源代码可以看出,它返回两个值,第一个是Key对应的值,第二个表示该key是否存在。如果Key不存在,则对应的返回值为nil,false
。
获取存储的所有键值对
如果我们想获取一个Reuqest上存储的所有键值对,我们可以使用context库提供的GetAll函数,它返回一个map对象,包含该Request上存储的所有键值对,现在我们使用该函数重写上面的示例。
1
2
3
4
5
6
7
8
9
func doHander (rw http.ResponseWriter, r * http.Request) {
//我们从这个Request里取出对应的值。
allParams:= context.GetAll (r)
user:= allParams["user" ].(string )
age:= allParams["age" ].(int )
rw.WriteHeader (http.StatusOK)
rw.Write ([]byte ("the user is " + user+ ",age is " + strconv.Itoa (age)))
}
copy
先获取所有键值对,然后使用map操作的方式,获取对应key的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
func GetAll (r * http.Request) map [interface {}]interface {} {
mutex.RLock ()
if context, ok := data[r]; ok {
result := make (map [interface {}]interface {}, len (context))
for k, v := range context {
result[k] = v
}
mutex.RUnlock ()
return result
}
mutex.RUnlock ()
return nil
}
copy
GetAll
函数的源代码逻辑很简单,但是这里有几个技巧,也是亮点。
我们根据data
的存储结构知道,data[r]
取出的就是一个map[interface{}]interface{}
,但是为什么要for循环一遍,返回一个新创建的map呢。这种做法是完全正确的,因为map是一个引用类型,如果我们直接返回了存储的map,调用者就可能会对这个map进行修改,破坏了map的存储,所以必须要返回一个map的拷贝,这是技巧亮点一 。
第二个亮点是map拷贝的时候,创建的拷贝map一定要指定大小,并且大小和原map一样,这样做的好处是map不用进行自动扩充,可以提高性能,result := make(map[interface{}]interface{}, len(context))
。
删除和清理
当我们附加的键值对不需要的时候,我们可以及时的把他们删除掉,这样可以释放内存,提高性能。context包提供了Delete
函数删除指定的Key,还提供了Clear
函数删除一个*http.Request
上所有的键值对。使用方法都很简单,这里不再举例。
1
2
3
context.Delete (r,key)
context.Clear (r)
copy
他们的函数实现原理都是对map的删除操作,因为数据存储本质上是一个map。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Delete (r * http.Request, key interface {}) {
mutex.Lock ()
if data[r] != nil {
delete (data[r], key)
}
mutex.Unlock ()
}
func Clear (r * http.Request) {
mutex.Lock ()
clear (r)
mutex.Unlock ()
}
func clear (r * http.Request) {
delete (data, r)
delete (datat, r)
}
copy
存储的生命周期
context存储键值对是有生命周期的,每个Request对应的存储map被创建的时候,都会记录该键值对设置的时间,这个时间是指该Request上所有键值对的时间,而不单单是哪一个键值对的时间。
1
2
3
4
5
6
7
8
9
func Set (r * http.Request, key, val interface {}) {
mutex.Lock ()
if data[r] == nil {
data[r] = make (map [interface {}]interface {})
datat[r] = time.Now ().Unix ()
}
data[r][key] = val
mutex.Unlock ()
}
copy
从上面的源代码可以看到,当一个request第一次被附加值的时候,记录该request对应的map的创建时间,存储在datat
这个map中。
1
2
3
var (
datat = make (map [* http.Request]int64 )
)
copy
有了这个时间,我们就知道这个request对应的map键值对被创建了多长时间,我们就可以清除被创建太久的键值对,这个函数就是content.Purge
,他可以保留最近maxAge
秒的键值对,超过这个时间的,都会被清理掉,然后返回清理的request个数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func Purge (maxAge int ) int {
mutex.Lock ()
count := 0
if maxAge <= 0 {
count = len (data)
data = make (map [* http.Request]map [interface {}]interface {})
datat = make (map [* http.Request]int64 )
} else {
min := time.Now ().Unix () - int64 (maxAge)
for r := range data {
if datat[r] < min {
clear (r)
count++
}
}
}
mutex.Unlock ()
return count
}
copy
从源代码中可以看到,如果我们传递 <=0 的值,那么会把所有request上存储的键值对全部删除掉。如果maxAge是 >0 的值,那么就会通过time.Now().Unix() - int64(maxAge)
算出一个最小的时间点min
,在这个时间之前创建的request对应的map键值对,都会被清理掉。
这个函数有很多应用场景,比如只保留最近一天在request上附加的值,超过一天就删除了,可以做一些周期性的任务工作。
多goroutine安全
差不多快结尾了,从上面的代码分析中,可以看出,数据库的存储都是在map中,这个map本身在多goroutine中是不安全的,所以我们保证它们的安全,context里所有函数的实现,都是用了读写锁,这样即可以提高读的效率,又可以保证写的安全。
1
2
3
var (
mutex sync.RWMutex
)
copy
可以留意,上面我们分析的几个函数源代码里都有,像Get函数,只用读锁,提高性能;Set等修改删除清理方法,都是写锁,保证数据安全。
自动清理存储的键值对
有时候,我们附加在一个*http.Request
上的键值对,只用一次,也就是这些键值对的生命周期,只有这次请求,用完就清理,如果是简单的请求处理链,我们知道哪一个处理是最后一步,执行完调用context.Clear
函数清理即可。
但是大部分时候我们都不知道哪段处理代码是最后一步,而且代码因为业务经常改动,可能又增加了一个函数,到时候忘记了调用清理,或者提前清理,都达不到我们的目的。
为了,context为我们提供了ClearHandler
函数,只需要把我们的Handler包装一下, 这个新的Hander就具有了自动清楚该Request上附加键值对的能力。
1
2
3
4
5
6
func ClearHandler (h http.Handler) http.Handler {
return http.HandlerFunc (func (w http.ResponseWriter, r * http.Request) {
defer Clear (r)
h.ServeHTTP (w, r)
})
}
copy
对一个Handler包装,返回的还是一个Handler,不影响前台的调用。这里留意defer Clear(r)
,不管这次请求的处理链有多长,代码都多少,都可以不管,最终请求处理完,清理存储的键值对就是,简单吧,也是一种技巧。刚刚例子中,就可以换成如下这种写法,达到自动清理的目的。
1
2
3
http.Handle ("/" ,http.HandlerFunc (myHander))
//换成
http.Handle ("/" ,context.ClearHandler (http.HandlerFunc (myHander)))
copy
新的替代者
自动Go1.7引入了context之后,这个库也停止维护了,因为标准库的context,完全可以替代他,满足我们的需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//定义一个Hander
func myHander (rw http.ResponseWriter, r * http.Request) {
//模拟为Request附加值,这里附加了2个
userContext:= context.WithValue (context.Background (),"user" ,"张三" )
ageContext:= context.WithValue (userContext,"age" ,18 )
rContext:= r.WithContext (ageContext)
//这个模拟一个方法或者函数的调用,大部分情况下可能不在一个包里
doHander (rw ,rContext)
}
func doHander (rw http.ResponseWriter, r * http.Request) {
//我们从这个Request里取出对应的值。
user:= r.Context ().Value ("user" ).(string )
age:= r.Context ().Value ("age" ).(int )
rw.WriteHeader (http.StatusOK)
rw.Write ([]byte ("the user is " + user+ ",age is " + strconv.Itoa (age)))
}
copy
这是使用Go标准库里的context包重写的,和我们前面的例子完全等价。这个主要在于,我们可以使用*Request.WithContext
函数,生成一个带有Context的*Request
,这样存储有键值对的Context
就跟着*Request
一起传递了,不管到哪里,都可以通过*Request.Context
函数获取附加在*Request
上的Context
,进而获取Context
上存储的键值对。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (r * Request) WithContext (ctx context.Context) * Request {
if ctx == nil {
panic ("nil context" )
}
r2 := new (Request)
* r2 = * r
r2.ctx = ctx
return r2
}
func (r * Request) Context () context.Context {
if r.ctx != nil {
return r.ctx
}
return context.Background ()
}
copy
小结
到这里,context库的分析结束了,这里可以学到的是一个简单的函数库的设计,map的复制性能,map引用类型的注意事项,以及多goroutine下数据读写的安全。
Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org
或者网站http://www.flysnow.org/ ,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。