go语言之context

context为go自带的包,在安装路径下的src/context文件夹中

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源,因此, Context 专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

Context 又被称为上下文,与 WaitGroup 不同的是,context 对于 goroutine 有更强的控制力,可以管理多级的 goroutine,可以简单理解为以下几点:

  • 当前协程取消了,可以通知所有由它创建的子协程退出
  • 当前协程取消了,不会影响到创建它的父级协程的状态
  • 扩展了额外的功能:超时取消、定时取消、可以和子协程共享数据

一、Context控制goroutine(取消)

1、下面例子通过context控制单个goroutine,如图:

  • 第22行:context.Background() 返回一个空的Context,这个空的Context一般作为整个Context树的根节点,然后使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine
  • 第23行:并发执行test函数,函数参数为Context接口,函数中通过for执行无限循环,内部通过select判断是否要结束,如果ctx.Done()接收到了值,说明goroutine结束,如果接收不到,继续运行
  • 第24行:休眠6秒,可以理解为test函数运行了6s,然后调用cancel函数停止goroutine
  • 第25行:执行取消函数,可以理解为go test执行6s中后再执行cancel(),它是CancelFunc类型的,也是WithCancel返回值,通过调用它就可以发出取消指令

运行结果如下:

如果不通过cancel手动来停止goroutine,我们看下会发生什么,把cancel注释,如图:

运行结果如下:

从上图可以看出,如果不通过cancel手动取消goroutine,那么go test会持续运行到主函数main自带的goroutine结束后一起退出

2、下面例子通过context控制多个goroutine,如图:

运行结果如下:

上面例子中启动了3个goroutine,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束,Context就像一个控制器一样,按下取消开关后,基于这个Context或者衍生的子Context都会收到通知,进行清理操作,最终释放goroutine,从而解决了goroutine启动后不可控以及占用资源的问题

二、Context接口(go版本v1.18.1)

Context的接口的方法如下:

type Context interface {
  Deadline() (deadline time.Time, ok bool)

  Done() <-chan struct{}

  Err() error

  Value(key any) any
}
  • Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消
  • Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源
  • Err方法返回取消的错误原因,因为什么Context被取消
  • Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的

上面方法中最常用的就是Done(),如果Context取消的时候,我们就可以得到一个关闭的chan,关闭的chan是可以读取的,所以只要可以读取的时候,就意味着收到Context取消的信号了,用法可参考上面例子

Context接口并不需要我们实现,Go内置已经帮我们实现了2个(安装路径下/src/context/context.go文件),代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context,如图:

  • emptyCtx实现了Context中的四个方法,因此实现了Context接口
  • Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context
  • TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个

两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context

下面是emptyCtx实现Context接口的方法,可以看到,这些方法返回都是nil或者零值,如图:

三、Context的继承衍生

有了上面的根Context,如何衍生更多的子Context?此时需要依靠context中的with系列函数,如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val any) Context
  • WithCancel:传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context
  • WithDeadline:和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然也可以不等到这个时候,提前通过取消函数进行取消
  • WithTimeout:和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思
  • WithValue:和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到

通过上面这些函数就可以创建一颗Context树,树的每个节点可以有任意多个子节点,节点层级可以有多个,上面四个with函数,第一个接收参数都是partent参数,也就是父Context,我们需要基于父Context创建出子Context,也可以理解为基于父Context的衍生

前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,定义如下:

type CancelFunc func()

这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级

四、通过WithValue传递元数据

  • 22行:定义key的名字为name,key为自定义的值,写成key100也可以
  • 26行:通过WithValue添加K-V键值对
  • 16行:通过Value()方法读取key的值

运行结果如下:

注:使用WithValue传值,一般是必须的值,不要什么值都传递

Context 使用原则:

  • 不要把Context放在结构体中,要以参数的方式传递
  • 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  • Context是线程安全的,可以放心的在多个goroutine中传递

五、Context实现超时功能:

1、下面例子创建子节点的context,5s后结束,如图:

  • 25行:创建过期时间为5s
  • 26行:根据根节点context创建子节点context,并设置超时时间为5s
  • 27:执行并发程序,通过ctx跟踪,因此执行5s后将收到取消通知

运行结果如下:

从上图看出,并发程序运行5s后收到了取消通知,因此执行ctx.Done()对应的case语句

标签