10 Go Context 上下文

一、Context概述

1.缘起

在开发web服务应用时,我们知道http启动的服务每接收到一个请求是便启动一个goroutine处理该request。而每个协程处理该请求是一般都会启动多个协程去处理不同任务,如调用RPC、访问数据库资源、缓存资源等等,这些协程都是为处理同一个request工作的,同时当request被取消或者超时的时候,从这个request处理协程创建的所有子协程也应该被结束。此时一个handler就必须对其启动的子协程有控制权,在context出现前,上述那些处理还是很丑陋的,有些甚至引起全局资源的滥用或者回调噩梦。context出现后,一切都得到解脱,context解决了处理同一生命周期协程树的资源管理问题。

2.官方解释:

Context,翻译为“上下文”,context包定义了Context接口类型,其接口签名方法定义了跨API边界和进程之间的执行最后期限、取消信号和其他请求范围的值。

对服务器的传入请求应创建Context类型,对服务器的传出调用应接受Context。它们之间的函数调用链必须传播Context,可以选择将其替换为使用WithCancel()、WithDeadline()、WithTimeout()或WithValue()创建的派生Context。当一个context被取消时,从它派生的所有context也被取消。WithCancel()、WithDeadline()和WithTimeout()函数接受上下文(父级)并返回派生上下文(子级)和Cancelfunc。调用Cancelfunc将取消子级及其子孙级,删除父级对子级的引用,并停止任何关联的计时器。如果不调用Cancelfunc,则会泄漏子级及其子孙级,直到父级被取消或计时器触发。Go-Vet工具检查取消功能是否用于所有控制流路径。

使用Context的程序应该遵循这些规则,以保持包之间的接口一致,并允许静态分析工具检查上下文传播:

//context传递的写法
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}
  • 不要将Context存储在结构类型中;而是将Context显式传递给每个需要它的函数。文应该是第一个参数,通常名为ctx:

  • 即使函数允许,也不要传递nil上下文。如果不确定要使用哪个上下文,请传递context.TODO(),该函数返回一个可被跟踪的顶级Context。

  • 只对传输进程和API的请求范围数据使用Context值,而不用于向函数传递可选参数。

  • 同一Context可以传递给在不同goroutine中运行的函数;上下文对于多个goroutine同时使用是安全的。

3.context包解析

我们来看一下Context接口的签名方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
3.1 Context接口签名方法解析:
  • Deadline() (deadline time.Time, ok bool)

Deadline方法返回应取消代表此上下文完成的工作的时间。

  • 未设置截止时间时,Deadline方法返回ok==false。
  • 对Deadline方法的连续调用将返回相同的结果。
  • Done() <-chan struct{}

done返回一个通道,该通道在应取消代表此上下文完成的工作时关闭。如果无法取消此上下文,则done可能返回nil。对done的连续调用返回相同的值。不同的派生Context对done通道关闭有不同的处理方式:

  • WithCancel()在调用cancel时安排关闭done;
  • WithDeadline()在截止时间过期时安排关闭done;
  • WithTimeout()在超时结束时安排关闭done。
  • Err() error

Err方法在done关闭后返回非零错误值。
返回值:

  • 如果上下文被取消,则返回Canceled;如果上下文的截止时间已过,则返回DeadLineExceeded;
  • 没有为err定义其他值。
  • 完成后关闭,对err的连续调用将返回相同的值。
  • Value(key interface{}) interface{}
  • 该方法可以让协程共享一些数据,获得数据是协程安全的。
  • 该方法返回与键的上下文关联的值,如果没有值与键关联,则返回nil。
  • 对具有相同键的值的连续调用返回相同的结果。
    仅对传输进程和API边界的请求范围数据使用上下文值,而不用于向函数传递可选参数。
    键标识上下文中的特定值。希望在上下文中存储值的函数通常在全局变量中分配一个键,然后使用该键作为context.WithValue() 和 Context.Value的参数。键可以是支持相等的任何类型;包应将键定义为未排序的类型以避免冲突。
3.2 顶级Context

context包提供两种顶级的上下文类型,由工厂方法创建:

(1).func Background() Context

context.Background()返回非零的空上下文。它从不被取消,没有值,也没有最后期限。它通常由主函数、初始化和测试使用,并且作为传入请求的顶级上下文。

(2).func TODO() Context

context.TODO()返回非零的空上下文。当不清楚要使用哪个上下文或者它还不可用时(因为周围的函数还没有被扩展以接受上下文参数),应该使用context.TODO()。静态分析工具可以识别TODO,它确定上下文是否在程序中正确传播。

两者区别:

==本质来讲两者区别不大,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播。==

3.3 派生Context

除以上两种顶级Context类型,context包提供四种创建可派生Context类型的函数:

(1). func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel函数返回具有新done通道的父级副本。当调用返回的cancel函数或关闭父上下文的done通道时(以先发生者为准),将关闭返回的上下文的done通道。
取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

官方使用示例:

// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
    dst := make(chan int)
    n := 1
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // returning not to leak the goroutine
            case dst <- n:
                n++
            }
        }
    }()
    return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
    fmt.Println(n)
    if n == 5 {
        break
    }
}

//OUTPUT:
1
2
3
4
5

(2). func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

WithDeadline函数返回父上下文的副本,其截止时间调整为不迟于d。如果父上下文的截止时间早于d,则WithDeadline(Parent,d)在语义上等同于父上下文。当截止时间到期、调用返回的cancel函数或关闭父上下文的done通道(以先发生者为准)时,返回的上下文的done通道将关闭。
取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

官方使用示例:
这个例子传递一个具有任意截止时间的上下文,告诉一个阻塞函数一旦到达它就应该放弃它的工作。

d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err())
}

//OUTPUT:
context deadline exceeded
(3). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消:

官方使用示例:这个例子传递一个带有超时的上下文,告诉一个阻塞函数它应该在超时结束后放弃它的工作。

// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}


//OUTPUT:
context deadline exceeded

*以上函数的特殊返回类型值:type CancelFunc func()

CancelFunc告诉操作放弃其工作。CancelFunc不等待工作停止。在第一次调用之后,对CancelFunc的后续调用不做任何操作。

(4). func WithValue(parent Context, key, val interface{}) Context

WithValue返回父级的副本,可为上下文设置一个键值对。
只对传输进程和API的请求范围数据使用上下文值,而不用于向函数传递可选参数。
提供的键必须是可比较的,并且不应是字符串或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给接口时进行分配,上下文键通常具有具体的类型结构。或者,导出的上下文键变量的静态类型应该是指针或接口。

官方使用示例:

type favContextKey string

f := func(ctx context.Context, k favContextKey) {
    if v := ctx.Value(k); v != nil {
        fmt.Println("found value:", v)
        return
    }
    fmt.Println("key not found:", k)
}

k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")

f(ctx, k)
f(ctx, favContextKey("color"))


//OUTPUT:
found value: Go
key not found: color

三、Context使用示例

使用context包来实现线程安全退出或超时的控制:


//定义一个并发worker
func worker(ctx context.Context, wg *sync.WaitGroup) error {
    defer wg.Done()

    for {
        select {
        //当父协程调用cancel()时,会从ctx.Done()得到struct{},此时返回ctx.Err()退出子线程
        case <-ctx.Done():
            return ctx.Err()
        default:
        //默认输出hello
        fmt.Println("hello")
        }
    }
}

func main() {
    //生成一个有超时控制的衍生Context,超时10s退出所有子协程
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(ctx, &wg)
    }
    
    //主协程1s后就cancel所有子协程了,每个worker都可以安全退出
    time.Sleep(time.Second)
    cancel()
    wg.Wait()
}

当并发体超时或main主动停止工作者Goroutine时,每个工作者都可以安全退出。

Go语言是带内存自动回收特性的,因此内存一般不会泄漏。当main函数不再使用管道时后台Goroutine有泄漏的风险。我们可以通过context包来避免这个问题,下面是防止内存泄露的素数筛实现:


// 返回生成自然数序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
    ch := make(chan int)
    go func() {
        for i := 2; ; i++ {
            select {
            //父协程cancel()时安全退出该子协程
            case <- ctx.Done():
                return
            //生成的素数发送到管道
            case ch <- i:
            }
        }
    }()
    return ch
}

// 管道过滤器: 删除能被素数整除的数
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
    out := make(chan int)
    go func() {
        for {
            if i := <-in; i%prime != 0 {
                select {
                //父协程cancel()时安全退出该子协程
                case <- ctx.Done():
                    return
                case out <- i:
                }
            }
        }
    }()
    return out
}

func main() {
    // 使用一个可由父协程控制子协程安全退出的Context。
    ctx, cancel := context.WithCancel(context.Background())

    ch := GenerateNatural(ctx) // 自然数序列: 2, 3, 4, ...
    
    for i := 0; i < 100; i++ {
        // 新出现的素数打印出来
        prime := <-ch 
        fmt.Printf("%v: %v\n", i+1, prime)
        // 基于新素数构造的过滤器
        ch = PrimeFilter(ctx, ch, prime) 
    }
    
    //输出100以内符合要求的素数后安全退出所有子协程
    cancel()
}

当main函数完成工作前,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。

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

推荐阅读更多精彩内容