• Go

Go Context

标准库中的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:
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()是用于如果您希望将来不再需要在此处创建上下文的情况,因为它将从外部传递,或者会有更多创建它的特定方法。

如果您不能决定,请不要大汗淋漓。实际上,它们的行为方式相同,因此请选择其中一个。


相关

最新