golang context tips

golang context tips
主要介绍golang context 包的基本使用以及需要注意的地方。

关于context的使用,官方blog是很好的参考文档,在1.6.2的时候还没有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的库中。context使用场景还是很广泛的,最典型的就是一个request过来,根据这个request要进行func1,func2,func3…一系列的处理,这些处理之间可能是相关联的,比如func2需要用到func1执行完成之后的信息,func3,func4又需要用到request传递进来的信息比如用户名或者密码,在这个时候这些信息就可以通过context库来进行包装,比如request时间到期的时候,func5,func6还没有是处理完这个时候可以统一让所有的二级操作停止下来不再执行。对于第二层的几个func来说,整个context就是一个共享的存储空间,这个存储空间的scope是这个request。个人感觉context的源码实现的简洁优雅,覆盖的场景也比较全面,遇到类似的场景直接使用context会减少一些编程的心智负担。

context包的源码分析

context的主要数据结构是一种嵌套的结构或者说是单向的继承关系的结构,比如最初的context是一个小盒子,里面装了一些数据,之后从这个context继承下来的children就像在原本的context中又套上了一个盒子,然后里面装着一些自己的数据。或者说context是一种分层的结构,根据使用场景的不同,每一层context都具备有一些不同的特性,这种层级式的组织也使得context易于扩展,职责清晰,下面仔细分析下1.8.1版本中的context包的实现:

1
2
3
4
5
6
7
8
9
10
type Context interface {
//返回这个context的失效时间,如果没有设置deadline 第二个ok参数应该返回false
Deadline() (deadline time.Time, ok bool)
//如果Done返回的channel被clsose,说明当前的这个context中的操作应该被结束。
Done() <-chan struct{}
//Done函数中返回的channel被close之后 Err中会返回相关的信息
Err() error
//传入的值是key值 返回的值是value值
Value(key interface{}) interface{}
}

以上是context接口暴露出来的基本能力。对于top-level的context,通过以下方式生成:

1
2
3
4
5
6
var (
background = new(emptyCtx)
)
func Background() Context {
return background
}

具体的emptyCtx虽然实现了上述Contextinterface的几个方法,但是方法体都为空,可以认为这个emptyCtx仅仅是为了继承而使用,没有具体的能力,类似的还有TODO方法,比如不太确定这个context是否要向下传递的时候就先使用TODO方法声明一个emptyCtx占一个位置。

withCancel

首先从实际使用的角度看一下withConcel的作用:

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
31
32
33
34
package main
import (
"context"
"fmt"
"time"
)
func doSth(ctx context.Context, index int) {
A:
for {
select {
case value, ok := <-ctx.Done():
if ok {
fmt.Println("get value", value)
} else {
break A
}
default:
fmt.Printf("goroutine %+v is doing sth\n", index)
time.Sleep(time.Second * 1)
}
}
fmt.Printf("do sth with index %+v finished\n", index)
}
func main() {
ctx := context.Background()
ctxWithCancel, cancelFunc := context.WithCancel(ctx)
go doSth(ctxWithCancel, 1)
go doSth(ctxWithCancel, 2)
go doSth(ctxWithCancel, 3)
time.Sleep(time.Second * 5)
fmt.Println("finish all doSth func")
cancelFunc()
time.Sleep(time.Second * 1)
}

在main函数中通过context.WithCancel生成了一个withCancel的实例以及一个cancelFuc,这个函数就是用来关闭ctxWithCancel中的 Done channel 函数,在后面的操作中,启动了三个goroutine,他们的第一个参数都是之前生成的ctxWithCanel实例,在函数内部通过for+select的方式监听channel,如果channel没有关闭就执行default的操作,做函数自己的事情,子函数只需要在自己的函数体中通过select不断地watch这个ctx中的channel,如果发现channel已经关闭(甚至都不需要通过这个channel发送任何数据)。就终止操作跳出循环函数执行结束。通常通过defer关键字控制cancelFunc的执行,在主函数最后关闭的时候关闭所有由这个主函数所启动的goroutine。

下面来粗略过一下WithCancelChannel部分的代码,初始化方法如下:

1
2
3
4
5
6
7
8
9
10
11
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

concelCtx实现了canceler接口

1
2
3
4
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

其中的Done方法会返回其中的done channel 而另外的cancel方法会关闭Done channel并且逐层向下遍历,关闭children的channel,并且将当前canceler从parent中移除。(这里的逻辑有些复杂)

WithCancel初始化一个concelCtx并且执行propagateCancel方法,最后返回一个concel function。看下propagateCancel方法:

可以看下cancelCtx的结构体定义,所有的children都存在一个map中。

1
2
3
4
5
6
7
8
9
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}

WithDeadLine

在withCancel的基础上进行的扩展,如果时间到了之后就进行cancel的操作,具体的操作流程基本上与withCancel一致,只不过控制cancel函数调用的时机是有一个timeout的channel所控制的。

WithTimeOut

返回withTimeout

1
2
3
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

WithValue

通常会map的形式来存储key,value,之后再将map放在context的 key,val的interface{}中。多层嵌套的key,value会使得查找效率变低。或者自定义一个struct,之后在struct中定义多个map和自定义的信息,之后把这个struct存储在val的interface{}中。一下两个链接是比较好的参考,这个)是关于key类型的问题,虽然直接使用string类型作为key值也不会报错,但是按照注释,最好不要使用string类型作为key值,比较好的方式像这样

此外注意iota的操作可以起到stirng到int类型的index的转换,可以参考userip包中使用的这样,额外构造一个自己的类型

1
2
3
4
5
6
//To avoid key collisions, userip defines an unexported type key and uses a value of this type as the context key:
// The key type is unexported to prevent collisions with context keys defined in other packages.
type key int
// userIPkey is the context key for the user IP address. Its value of zero is arbitrary.
// If this package defined other context keys, they would have different integer values.
const userIPKey key = 0

context的一些具体使用案例

通过context设置cookie

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func AddContextSupport(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, "-", r.RequestURI)
cookie, _ := r.Cookie("username")
if cookie != nil {
ctx := context.WithValue(r.Context(), "username", cookie.Value)
// WithContext returns a shallow copy of r with its context changed
// to ctx. The provided ctx must be non-nil.
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
expitation := time.Now().Add(24 * time.Hour)
var username string
if username = r.URL.Query().Get("username"); username == "" {
username = "guest"
}
cookie := http.Cookie{Name: "username", Value: username, Expires: expitation}
http.SetCookie(w, &cookie)
}
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
expiration := time.Now().AddDate(0, 0, -1)
cookie := http.Cookie{Name: "username", Value: "alice_cooper@gmail.com", Expires: expiration}
http.SetCookie(w, &cookie)
}
func StatusHandler(w http.ResponseWriter, r *http.Request) {
if username := r.Context().Value("username"); username != nil {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hi username:" + username.(string) + "\n"))
} else {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Logged in"))
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", StatusHandler)
mux.HandleFunc("/login", LoginHandler)
mux.HandleFunc("/logout", LogoutHandler)
contextedMux := AddContextSupport(mux)
log.Fatal(http.ListenAndServe(":9999", contextedMux))
}

上面的例子展示了通过context控制结合cookie控制login以及logout的操作,需要提前了解一点servmux以及wrapper function的知识。

具体来看,在main函数中通过HandleFun注册了三个router,最后对mux进行了包装,返回了新的实现了http.Handler的实例。

在login的时候通过get方式传入的用户名设置cookie并将包含了用户信息的cookie写回到response中记录在client上。

之后在开启server的时候,通过wrapper的方式进行判断,如果有cookie信息就将其写入到request中,之后再启动,如果没有就直接启动,这样每次过来的request就都携带了context信息,cookie中的信息通过context的形式具体被server记录下来。这里具体使用的是valueCtx,其中包含了interface{}形式的key-value,方便用户自定义存储结构。

1
2
3
4
type valueCtx struct {
Context
key, val interface{}
}

在logout的时候通过设置cookie的过期时间使得cookie失效。

参考资料

官方blog中的介绍
https://blog.golang.org/context

context源码解读
http://blog.csdn.net/xiaohu50/article/details/49100433

这一篇中有较多实例
http://jsmean.com/blog/post/5859fd8cec9803b957866ce9

context的一些使用例子
http://www.nljb.net/default/Golang%E4%B9%8BContext%E7%9A%84%E4%BD%BF%E7%94%A8/

较多例子
http://www.tuicool.com/articles/r6RZzeM

推荐文章