go context

[TOC]
相信大家在日常工作开发中一定会看到这样的代码:

func a1(ctx context ...){
  b1(ctx)
}
func b1(ctx context ...){
  c1(ctx)
}
func c1(ctx context ...)

context被当作第一个参数(官方建议),并且不断透传下去,基本一个项目代码中到处都是context,但是你们真的知道它有何作用吗以及它是如何起作用的吗?我记得我第一次接触context时,同事都说这个用来做并发控制的,可以设置超时时间,超时就会取消往下执行,快速返回,我就单纯的认为只要函数中带着context参数往下传递就可以做到超时取消,快速返回。相信大多数初学者也都是和我一个想法,其实这是一个错误的思想,其取消机制采用的也是通知机制,单纯的透传并不会起作用,比如你这样写代码:

func main()  {
 ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
 defer cancel()
 go Monitor(ctx)

 time.Sleep(20 * time.Second)
}

func Monitor(ctx context.Context)  {
 for {
  fmt.Print("monitor")
 }
}

即使context透传下去了,没有监听取消信号也是不起任何作用的。所以了解context的使用还是很有必要的,本文就先从使用开始,逐步解析Go语言的context包,下面我们就开始喽!!!

context 包的起源与作用

看官方博客我们可以知道context包是在go1.7版本中引入到标准库中的:

context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,context包定义了上下文类型,可以使用background、TODO创建一个上下文,在函数调用链之间传播context,也可以使用WithDeadline、WithTimeout、WithCancel 或 WithValue 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。

目前我们常用的一些库都是支持context的,例如gin、database/sql等库都是支持context的,这样更方便我们做并发控制了,只要在服务器入口创建一个context上下文,不断透传下去即可。

context 的使用

创建context

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;

所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的With系列函数来进行派生:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

这四个函数都要基于父Context衍生,通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,画个图表示一下:

image.png

基于一个父Context可以随意衍生,其实这就是一个Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background衍生出四个子context:ctx1.0-cancel、ctx2.0-deadline、ctx3.0-timeout、ctx4.0-withvalue,这四个子context还可以作为父context继续向下衍生,即使其中ctx1.0-cancel 节点取消了,也不影响其他三个父节点分支。

创建context方法和context的衍生方法就这些,下面我们就一个一个来看一下他们如何被使用。

WithValue携带数据

我们日常在业务开发中都希望能有一个trace_id能串联所有的日志,这就需要我们打印日志时能够获取到这个trace_id,在python中我们可以用gevent.local来传递,在java中我们可以用ThreadLocal来传递,在Go语言中我们就可以使用Context来传递,通过使用WithValue来创建一个携带trace_id的context,然后不断透传下去,打印日志时输出即可,来看使用例子:

我们基于context.Background创建一个携带trace_id的ctx,然后通过context树一起传递,从中派生的任何context都会获取此值,我们最后打印日志的时候就可以从ctx中取值输出到日志中。目前一些RPC框架都是支持了Context,所以trace_id的向下传递就更方便了。

在使用withVaule时要注意四个事项:

不建议使用context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。

因为携带value也是key、value的形式,为了避免context因多个包同时使用context而带来冲突,key建议采用内置类型。

上面的例子我们获取trace_id是直接从当前ctx获取的,实际我们也可以获取父context中的value,在获取键值对是,我们先从当前context中查找,没有找到会在从父context中查找该键对应的值直到在某个父context中返回 nil 或者查找到对应的值。

context传递的数据中key、value都是interface类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。

超时控制

通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web框架或rpc框架都会采用withTimeout或者withDeadline来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeout和withDeadline作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context,这里要注意的是他们都会返回一个cancelFunc方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc去停止定时减少不必要的资源浪费。

withTimeout、WithDeadline不同在于WithTimeout将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout内部也是调用的WithDeadline。

现在我们就举个例子来试用一下超时控制,现在我们就模拟一个请求写两个例子:

达到超时时间终止接下来的执行

package main

import (
    "context"
    "fmt"
    "time"
)

func main()  {
    HttpHandler()
}

func NewContextWithTimeout() (context.Context,context.CancelFunc) {
    return context.WithTimeout(context.Background(), 3 * time.Second)
}

func HttpHandler()  {
    ctx, cancel := NewContextWithTimeout()
    defer cancel()
    deal(ctx)
}

func deal(ctx context.Context)  {
    for i:=0; i< 10; i++ {
        time.Sleep(1*time.Second)
        select {
        case <- ctx.Done():
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("deal time is %d\n", i)
        }
    }
}

没有达到超时时间终止接下来的执行

使用起来还是比较容易的,既可以超时自动取消,又可以手动控制取消。这里大家要记的一个坑,就是我们往从请求入口透传的调用链路中的context是携带超时时间的,如果我们想在其中单独开一个goroutine去处理其他的事情并且不会随着请求结束后而被取消的话,那么传递的context要基于context.Background或者context.TODO重新衍生一个传递,否决就会和预期不符合了,可以看一下我之前的一篇踩坑文章:context使用不当引发的一个bug。

withCancel取消控制

日常业务开发中我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。

我们使用withCancel创建一个基于Background的ctx,然后启动一个讲话程序,每隔1s说一话,main函数在10s后执行cancel,那么speak检测到取消信号就会退出。

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

推荐阅读更多精彩内容

  • 1 什么是Context 最近在公司分析gRPC源码,proto文件生成的代码,接口函数第一个参数统一是ctx c...
    淘小铺刀仔阅读 322评论 0 1
  • 控制并发有两种经典的方式:WaitGroup和Context WaitGroup:控制多个Goroutine同时完...
    JunChow520阅读 668评论 0 2
  • 使用Go作为服务端开发时,每个请求过来都会分配一个goroutine来处理,请求处理过程中,可能还会创建额外的go...
    光华路程序猿阅读 298评论 0 0
  • context包是Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体,是开发常用的并发控制技术。与Wa...
    Dakini_Wind阅读 3,425评论 0 2
  • context上下文的意思, 可以用来做超时管理 ,context 可以设置截止日期,超时或调用取消函数来通知所有...
    Elorybird阅读 271评论 0 0