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

飞雪无情 · · 832 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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() {
//启动一个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被传递到哪里去,都可以获得我们存储的值,为我们为值得传递提供了便利。

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) {
//我们从这个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
//定义一个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上存储的键值对。

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

扫码关注


有疑问加站长微信联系(非本文作者)

本文来自:飞雪无情的博客

感谢作者:飞雪无情

查看原文:Go语言经典库使用分析(二)| Gorilla Context

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

832 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传