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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值