在GO中,我们需要有能力管理并发运行中的goroutine,主要是指它的生命周期。那些失去控制的goroutine可能会进入某个死循环,从而导致其它等待中的goroutine死锁或运行太久。理想情况是,可以终止这些goroutine或使它们不太好的超时退出。
可以基于context编程。Go 1.7 引入了context包。它为我们提供了这些能力,同时我们也可以将某些变量与context关联实现信息的跨界交流与传递。
在本教程中,你将会了解到context的输入输出以及何时和如何使用它,以避免滥用。
什么情况下使用
context是一种非常好的抽象。它让你可以封装一些与核心逻辑无关的信息,比如 请求ID、认证Token和超时时间。这可以为我们带来如下的一些好处:
- 它有效地帮助我们把核心逻辑参数与运行参数中分离开来。
- 它为我们制定了通用的操作规则和在边界交流数据的方法。
- 它为我们提供了一套标准的机制,在不修改函数签名的情况下传递额外信息。
Context接口
如下是Context的所有接口信息:
type Context interface { Deadline() (deadline time.Time, ok bool) Done <-chan struct{} Err() error Value(key interface{}) interface{} }
下面介绍各个方法的作用。
Deadline()
当执行完成,context就应被取消,此时Deadline()会返回相应的时间。当没有设置最后期限,Deadline返回 ok == false
。多次调用Deadline返回结果相同。
Done()
Done()方法返回的是一个channel,它将在工作执行完成即context应该被取消的时候被关闭。连续调用Done()返回的结果相同。
- context.WithCancel()返回cancel函数,当调用它时,Done会被关闭;
- context.WithDeadline()设置过期时间,当过期后,Done会被关闭;
- context.WithTimeout()设置超时时间,当超时后,Done会被关闭;
可以在select语句中使用Done:
func Stream(ctx context.Context, out chan<- Value) error { for { v, err := DoSomething(ctx) if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case out <- v: } } }
可以读下这篇文章 Go并发模型:Pipeline和Cancellation ,介绍了很多如何使用Done取消context的例子。
Err()
只要Done是打开状态,Err()返回nil。如果context被取消,它返回 Canceled
error。如果context到期或超时,它返回 DeadlineExceeded
error。Done被关闭后,多次调用Err()返回结果相同。下面是一些定义:
// Canceled is the error returned by Context.Err when the context is canceled. var Canceled = errors.New("context canceled") // DeadlineExceeded is the error returned by Context.Err when the context's deadline passes. var DeadlineExceeded error = deadlineExceededError{}
Value()
Value()通过key去调用与context关联的value,如果context中与指定key对应的值,返回nil。多次以相同key调用Value()返回结果相同。
Context中的Value仅仅用于在请求范围内不同程序和接口的数据转化,不可用于其他的参数传递。
在Context,一个key代表一个具体的值。那些希望用Context存储数据值的函数通常会在全局分配一个变量key,并用这个key作为参数调用context.WithValue和context.Value()。key支持任何类型。
Context的作用域
Contexts有作用范围。你可以从已有的context作用域延伸出新的作用域。父级不能访问衍生的作用域的数据,不过下级是可以访问父级作用域数据的。
Contexts是层级结构。你可以通过context.Background()或context.TODO()创建contexts。无论何时你调用WithCancel、WithDeadline或WithTimeout,都会得到出新的context,同时会返回一个cancel函数。最重要的是当父级的context被取消,所有的子级也将取消。
你应该在main、init和tests中使用context.Background()。如果不知道该使用什么context,可以通过context.TODO()产生context。
注意,Background和TODO生成的context是不可取消的。
过期、超时和取消
如你所知,WithDeadline() 和 WithTimeout() 创建的contexts将会自动取消,而WithCancel() 创建的context必须通过cancel()明确指定何时取消。其实,它们都会返回一个cancel函数,所以既没有超时/过期,你依然可以通过cancel取消衍生的context。
让我们看个例子。首先,contextDemo函数有两个参数,分别是name和context。它在一个无限循环中运行,不停的在控制台打印name和deadline(如果有的话)。然后sleep一秒。
package main import ( "fmt" "context" "time" ) func contextDemo(name string, ctx context.Context) { for { if ok { fmt.Println(name, "will expire at:", deadline) } else { fmt.Println(name, "has no deadline") } time.Sleep(time.Second) } }
主函数创建了三个contexts:
- 三秒超时的timeoutContext;
- 没有过期时间的cancelContext;
- 由cancelContext产生的从现在开始4小时过期的deadlineContext;
然后,启动三个contextDemo的goroutine。它们并发执行且每秒打印一次message。
主函数通过读取timeoutContext的Done()来实现等待goroutine超时退出。一但三秒超时,main函数就调用cancelFunc取消cancelContext中的goroutine,同时cancelContext衍生出来的4小时过期的deadlineContext的goroutine也将退出。
func main() { timeout := 3 * time.Second deadline := time.Now().Add(4 * time.Hour) timeOutContext, _ := context.WithTimeout( context.Background(), timeout) cancelContext, cancelFunc := context.withCancel( context.Background()) deadlineContext, _ := context.WithDeadline( cancelContext, deadline) go contextDemo("[timeoutContext]", timeOutContext) go contextDemo("[cancelContext]", cancelContext) go contextDemo("[deadlineContext]", deadlineContext) // Wait for the timeout to expire <- timeOutContext.Done() // This will cancel the deadline context as well as its // child - the cancelContext fmt.Println("Cancelling the cancel context...") cancelFunc() <- cancelContext.Done() fmt.Println("The cancel context has been cancelled...") // Wait for both contexts to be cancelled <- deadlineContext.Done() fmt.Println("The deadline context has been cancelled...") }
下面是输出结果:
[cancelContext] has no deadline [deadlineContext] will expire at: 2017-07-29 09:06:02.34260363 [timeoutContext] will expire at: 2017-07-29 05:06:05.342603759 [cancelContext] has no deadline [timeoutContext] will expire at: 2017-07-29 05:06:05.342603759 [deadlineContext] will expire at: 2017-07-29 09:06:02.34260363 [cancelContext] has no deadline [timeoutContext] will expire at: 2017-07-29 05:06:05.342603759 [deadlineContext] will expire at: 2017-07-29 09:06:02.34260363 Cancelling the cancel context... The cancel context has been cancelled... The deadline context has been cancelled...
输出结果不变。接下来是最佳实践章节,将介绍一些指导原则,以便于我们恰当地使用context数据传递。
最佳实践
围绕context数据传递的几个最佳实践:
- 避免在context中传递函数参数;
- 在全局变量中为context中的数据分配一个对应key;
- 包中应该为key定义一个不可导出的类型,以防止发生冲突;
- 包中定义的key应该为其在context存储的数据提供类型安全访问方法;
HTTP请求的Context
context的常用场景之一就是在HTTP请求间传递信息。这些信息可能包含请求ID、认证证书等。在GO1.7,标准库net/http利用了context的优势,并且已经标准化,直接在request中加入了对context的支持。
func (r *Request) Context() context.Context func (r *Request) WithContext(ctx context.Context) *Request
现在,我们可以使用一种标准方式把从headers中获取到的requestId传递到最终的处理函数。WithRequestID() 处理函数从"X-Request-ID"头部导出requestID并从正在使用的context中衍生出一个带有requestID的context。然后把它传递给调用链的下一个处理函数。公共函数GetRequestID()为处理函数提供了访问RequestID的途径,包括定义在其他包的处理函数。
const requestIDKey int = 0 func WithRequestID(next http.Handler) http.Handler { return http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { // Extract request ID from request header reqID := req.Header.Get("X-Request-ID") // Create new context from request context with // the request ID ctx := context.WithValue( req.Context(), requestIDKey, reqID) // Create new request with the new context req = req.WithContext(ctx) // Let the next handler in the chain take over. next.ServeHTTP(rw, req) } ) } func GetRequestID(ctx context.Context) string { ctx.Value(requestIDKey).(string) } func Handle(rw http.ResponseWriter, req *http.Request) { reqID := GetRequestID(req.Context()) ... } func main() { handler := WithRequestID(http.HandlerFunc(Handle)) http.ListenAndServe("/", handler) }
总结
基于Context的编程为我们提供了一套标准和良好支持的方法,它解决了两个常见的问题:goroutine的生命周期管理和信息传递。
以最佳实践为准,在合适的场景下使用contexts,你的编码能力将会大幅提升。