简书不维护了,欢迎关注我的知乎:波罗学的个人主页
原文:Context-Based Programming in Go
作者:Gigi Sayfan
在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,你的编码能力将会大幅提升。