标准库中的context包提供了Context类型,它有多种用途。
下面是context.Context几个典型用法:
- 带超时(截止时间)的上下文是对可能会花费很长时间的函数实施超时的一种通用方法,在这种情况下,如果它们超过超时,我们希望可以选择中止它们
- 取消上下文是取消goroutine的通用方法
- 带值的上下文是将任意值与上下文关联的一种方式
创建context
在大多数情况下,您将调用需要context.Context的现有API。
如果您没有,请使用context.TODO()或context.Background()函数创建它。 了解差异。
context.Context是一个不可变的(只读)值,因此您无法对其进行修改。
创建例如 带有值的上下文,您调用context.WithValue()会返回一个新的上下文,该上下文将包装现有上下文并添加其他信息。
context.Context是一个接口,因此您可以传递nil,但不建议这样做。
许多API期望使用非null值,如果传递nil将会崩溃,因此最好始终传递使用context.Background()或context.TODO()创建的API。
没有性能问题,因为这些函数返回共享的全局变量(是不变的!)。
使用带超时的上下文设置HTTP请求的超时
重复执行HTTP客户端文章中的示例,这是一种创建超时的上下文的方法,以确保HTTP GET请求不会永远挂起:
// httpbin.org is a service for testing HTTP client
// this URL waits 3 seconds before returning a response
uri := "https://httpbin.org/delay/3"
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
log.Fatalf("http.NewRequest() failed with '%s'\n", err)
}
// create a context indicating 100 ms timeout
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
// get a new request based on original request but with the context
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// the request should timeout because we want to wait max 100 ms
// but the server doesn't return response for 3 seconds
log.Fatalf("http.DefaultClient.Do() failed with:\n'%s'\n", err)
}
defer resp.Body.Close()
HTTP客户端知道如何使用超时来解释上下文。 您只需要创建并提供它。
有值的环境
在HTTP服务器中,每个请求都由在其自己的goroutine中运行的处理函数提供服务。
我们通常希望以方便的方式提供按要求提供的通用信息。
例如,在开始处理请求时,我们可能会检查Cookie以查看登录用户是否提出了请求,并且我们希望在任何地方都可以使用用户信息。
我们可以通过使用带有值的上下文来做到这一点:
type User struct {
Name string
IsLoggedIn bool
}
type userKeyType int
var userKey userKeyType
func contextWithUser(ctx context.Context, user *User) context.Context {
return context.WithValue(ctx, userKey, user)
}
// returns nil if not set
func getUserFromContext(ctx context.Context) *User {
user, ok := ctx.Value(userKey).(*User)
if !ok {
return nil
}
return user
}
// will panic if not set
func mustGetUserFromContext(ctx context.Context) *User {
return ctx.Value(userKey).(*User)
}
func printUser(ctx context.Context) {
user := getUserFromContext(ctx)
fmt.Printf("User: %#v\n", user)
}
func main() {
ctx := context.Background()
user := &User{
Name: "John",
IsLoggedIn: false,
}
ctx = contextWithUser(ctx, user)
printUser(ctx)
}
User: &main.User{Name:"John", IsLoggedIn:false}
为了使示例清晰起见,我们仅显示使用值创建上下文并从上下文中检索值。
由于上下文值是接口{},因此最好编写类型安全的包装函数来设置和检索值。
用来设置/获取值的键也是一个接口{}。 由于可以将上下文传递给您未编写的代码中的函数,因此您要确保用于键的值是唯一的。
这就是为什么我们定义非导出类型的userKeyType并使用该类型的非导出全局变量userKey的原因。
这样可以确保我们程序包外部的代码无法使用此密钥。
如果密钥是例如,这不是真的。 字符串(或可用于多个软件包的任何类型)。
我们编写了两个函数来检索值。
如果未设置值,则一个恐慌,另一个则返回nil。
使用哪个是您代码的政策决定。
有时在上下文上缺少变量意味着您的程序存在错误,您应该使用mustGetUserFromContext变体,在这种情况下会出现恐慌。
有时会缺少变量,可以使用getUserFromContext变体。
编写可取消的函数
使用接受可取消上下文的现有功能很容易。
编写可以通过上下文取消的函数要困难得多。
当时间到期或调用context.WithCancel()或context.WithTimeout()返回的cancel函数时,会在上下文中发出一个通道信号。
编写可取消函数时,您必须定期检查context.Done()返回的通道,并在收到信号后立即返回。
它确实使代码笨拙:
func longMathOp(ctx context.Context, n int) (int, error) {
res := n
for i := 0; i < 100; i++ {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
res += i
// simulate long operation by sleeping
time.Sleep(time.Millisecond)
}
}
return res, nil
}
func main() {
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*200)
res, err := longMathOp(ctx, 5)
fmt.Printf("Called longMathOp() with 200ms timeout. res; %d, err: %v\n", res, err)
ctx, _ = context.WithTimeout(context.Background(), time.Millisecond*10)
res, err = longMathOp(ctx, 5)
fmt.Printf("Called longMathOp() with 10ms timeout. res: %d, err: %v\n", res, err)
}
Called longMathOp() with 200ms timeout. res; 4955, err: <nil>
Called longMathOp() with 10ms timeout. res: 0, err: context deadline exceeded
为了清楚起见,这是人为的任务。
我们的longMathOp函数执行100次简单操作,并通过在每次迭代中休眠1 ms来模拟慢度。
我们可以预期大约需要100毫秒。
具有默认子句的select是非阻塞的。 如果ctx.Done()通道中没有任何内容,我们就不必等待值,而是立即执行默认部分,这就是程序逻辑所在的地方。
我们可以在测试中看到,如果超时大于100 ms,则函数结束。
如果超时小于100毫秒,则发出ctx.Done()通道信号,我们在longMathOp中检测到它并返回ctx.Err()。
上下文是价值树
通过包装现有的不可变上下文并添加其他信息来创建上下文。
由于您可以多次“分支”同一上下文,因此可以将上下文值视为值树。
下面的树:
ctx := context.WithValue(
context.WithDeadline(
context.WithValue(context.Background(), sidKey, sid),
time.Now().Add(30 * time.Minute),
),
ridKey, rid,
)
trCtx := trace.NewContext(ctx, tr)
logCtx := myRequestLogging.NewContext(ctx, myRequestLogging.NewLogger())
可以形象化为:
上下文表示为有向图
每个子上下文都可以访问其父上下文的值。
数据访问在树中向上流动(由黑色边缘表示)。
取消信号沿着树传播。 如果取消了上下文,则其所有子级也将被取消。
抵消信号流由灰色边缘表示。
context.TODO() vs. context.Background()
您可以使用context.TODO()和context.Background()创建新的空上下文。
有什么不同?
在功能方面:无。它们是逐位完全相同的值。
区别在于目的。
官方文档将context.TODO()描述为:
TODO返回一个非空的Context。当不清楚要使用哪个上下文或尚不可用时(因为尚未扩展周围的功能以接受Context参数),代码应使用context.TODO。静态分析工具可识别TODO,该工具可确定上下文是否在程序中正确传播。
和context.Background()为:
Background返回一个非空的Context。它永远不会被取消,没有价值,也没有期限。它通常由主要功能,初始化和测试使用,并用作传入请求的顶级上下文。
坦白说,我不确定他们想说什么。
我猜context.TODO()是用于如果您希望将来不再需要在此处创建上下文的情况,因为它将从外部传递,或者会有更多创建它的特定方法。
如果您不能决定,请不要大汗淋漓。实际上,它们的行为方式相同,因此请选择其中一个。