微信号:flysnow_org

介绍:打杂师,观察家.

Go语言经典库使用分析(二)| Gorilla Context

2017-07-29 22:08 飞雪无情

Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org或者网站http://www.flysnow.org/,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。

在Go1.7之前,Go标准库还没有内置Context的时候,如果我们想在一个Http.Request里附加值,怎么做呢?一般都是Map对象,存储对应的Request以及附加的值,然后在需要的时候取出来,今天我们介绍的这个就是实现了一个类似于这样功能的库,因为比较简单,而且实用,所以就先选择它来分析。

安装

要使用这个库,需要先安装,在Go里,任何库的安装都是一样的,那就是通过go get。

$ go get github.com/gorilla/context

安装之后,我们就可以使用了,下面来看一个存储数据,取出数据的例子

package mainimport (
   "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))) }

数据如何存储

上面是一个很简单的示例,用过context.Setcontext.Get函数为一个*http.Request附加我们想存储的键值对,在需要的时候取出他们。这样不管我们的*http.Request被传递到哪里去,都可以获得我们存储的值,为我们为值得传递提供了便利。

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对象里。

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:

data[r] = make(map[interface{}]interface{})

创建好了之后,就可以为这个map附加值了。通过data[r][key] = val进行赋值,就完成了一次存储。这里使用的是map,如果存储的时候,已经有了旧值,旧的值会被新的覆盖。

获取存储的值

现在我们知道,存储值得对象其实是个map,所以获取存储的值也比较简单了,像操作map一样获取值即可,现在看下context为我们提供的获取值得函数代码。

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是否存在。

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上存储的所有键值对,现在我们使用该函数重写上面的示例。

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))) }

先获取所有键值对,然后使用map操作的方式,获取对应key的值。

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上所有的键值对。使用方法都很简单,这里不再举例。

context.Delete(r,key)

context.Clear(r)

他们的函数实现原理都是对map的删除操作,因为数据存储本质上是一个map。

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上所有键值对的时间,而不单单是哪一个键值对的时间。

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中。

var (
    datat = make(map[*http.Request]int64)
)

有了这个时间,我们就知道这个request对应的map键值对被创建了多长时间,我们就可以清除被创建太久的键值对,这个函数就是content.Purge,他可以保留最近maxAge秒的键值对,超过这个时间的,都会被清理掉,然后返回清理的request个数。

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里所有函数的实现,都是用了读写锁,这样即可以提高读的效率,又可以保证写的安全。

var (
    mutex sync.RWMutex
)

可以留意,上面我们分析的几个函数源代码里都有,像Get函数,只用读锁,提高性能;Set等修改删除清理方法,都是写锁,保证数据安全。

自动清理存储的键值对

有时候,我们附加在一个*http.Request上的键值对,只用一次,也就是这些键值对的生命周期,只有这次请求,用完就清理,如果是简单的请求处理链,我们知道哪一个处理是最后一步,执行完调用context.Clear函数清理即可。

但是大部分时候我们都不知道哪段处理代码是最后一步,而且代码因为业务经常改动,可能又增加了一个函数,到时候忘记了调用清理,或者提前清理,都达不到我们的目的。

为了,context为我们提供了ClearHandler函数,只需要把我们的Handler包装一下, 这个新的Hander就具有了自动清楚该Request上附加键值对的能力。

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),不管这次请求的处理链有多长,代码都多少,都可以不管,最终请求处理完,清理存储的键值对就是,简单吧,也是一种技巧。刚刚例子中,就可以换成如下这种写法,达到自动清理的目的。

http.Handle("/",http.HandlerFunc(myHander))

//换成
http.Handle("/",context.ClearHandler(http.HandlerFunc(myHander)))

新的替代者

自动Go1.7引入了context之后,这个库也停止维护了,因为标准库的context,完全可以替代他,满足我们的需求。

//定义一个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))) }

这是使用Go标准库里的context包重写的,和我们前面的例子完全等价。这个主要在于,我们可以使用*Request.WithContext函数,生成一个带有Context的*Request,这样存储有键值对的Context就跟着*Request一起传递了,不管到哪里,都可以通过*Request.Context函数获取附加在*Request上的Context,进而获取Context上存储的键值对。

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/,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。


 
飞雪无情 更多文章 Go语言经典库使用分析(一)| 开篇 《Go语言实战》笔记(二十八) | 后记 水库文章目录 《Go语言实战》笔记(二十七) | Go unsafe Pointer 《Go语言实战》笔记(二十六) | Go unsafe 包之内存布局
猜您喜欢 第四十八讲 排序算法(3) 多线程 从0开始写作赚20万,他把文字当产品经营 最令程序员沮丧的 10 件事 深夜换脸啦,神秘的程序员们头像包第三发