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
|
安装之后,我们就可以使用了,下面来看一个存储数据,取出数据的例子
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() { http.Handle("/",http.HandlerFunc(myHander)) http.ListenAndServe(":1234",nil) } func myHander(rw http.ResponseWriter, r *http.Request) { context.Set(r,"user","张三") context.Set(r,"age",18) doHander(rw ,r) } func doHander(rw http.ResponseWriter, r *http.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))) }
|
数据如何存储
上面是一个很简单的示例,用过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() }
|
Set函数接受三个参数,第一个是*http.Request
,第二个是Key,第三个是值,从这三个参数看,我们可以为一个*http.Request
存储多个值。
值存储在什么地方呢?context库里使用的是map对象里。
1 2 3
| var ( data = make(map[*http.Request]map[interface{}]interface{}) )
|
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{})
|
创建好了之后,就可以为这个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 }
|
源代码逻辑上做了一些判断,存在就返回值,不存在就返回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 }
|
从上面的源代码可以看出,它返回两个值,第一个是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) { 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))) }
|
先获取所有键值对,然后使用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 }
|
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)
|
他们的函数实现原理都是对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) }
|
存储的生命周期
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() }
|
从上面的源代码可以看到,当一个request第一次被附加值的时候,记录该request对应的map的创建时间,存储在datat
这个map中。
1 2 3
| var ( datat = make(map[*http.Request]int64) )
|
有了这个时间,我们就知道这个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 }
|
从源代码中可以看到,如果我们传递 <=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 )
|
可以留意,上面我们分析的几个函数源代码里都有,像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) }) }
|
对一个Handler包装,返回的还是一个Handler,不影响前台的调用。这里留意defer Clear(r)
,不管这次请求的处理链有多长,代码都多少,都可以不管,最终请求处理完,清理存储的键值对就是,简单吧,也是一种技巧。刚刚例子中,就可以换成如下这种写法,达到自动清理的目的。
1 2 3
| http.Handle("/",http.HandlerFunc(myHander)) http.Handle("/",context.ClearHandler(http.HandlerFunc(myHander)))
|
新的替代者
自动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
| func myHander(rw http.ResponseWriter, r *http.Request) { 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) { 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))) }
|
这是使用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() }
|
小结
到这里,context库的分析结束了,这里可以学到的是一个简单的函数库的设计,map的复制性能,map引用类型的注意事项,以及多goroutine下数据读写的安全。
Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org
或者网站http://www.flysnow.org/,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。
有疑问加站长微信联系(非本文作者)