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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • Go标准库Context 在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去...
    吴佳浩阅读 369评论 0 2
  • 控制并发有两种经典的方式:WaitGroup和Context WaitGroup:控制多个Goroutine同时完...
    JunChow520阅读 660评论 0 2
  • 本文从上下文Context、同步原语与锁、Channel、调度器四个方面介绍Go语言是如何实现并发的。本文绝大部分...
    彦帧阅读 1,557评论 1 3
  • WHY 每一个长请求都应该有个超时限制需要在调用中传递这个超时比如开始处理请求的时候我们说是 3 秒钟超时那么在函...
    林桉阅读 307评论 0 0
  • 1 什么是Context 最近在公司分析gRPC源码,proto文件生成的代码,接口函数第一个参数统一是ctx c...
    淘小铺刀仔阅读 320评论 0 1