大家好,我是建宇。上下文是Go语言的一个非常显着的特性。新的标准库上下文在Go1.7中正式引入。它的主要功能是传递goroutine中的context,传递信息包括goroutine运行控制和context信息传递等功能。为了加强大家对Go语言上下文的设计,本文将对标准库上下文进行深入分析,看看它到底隐含了什么,为什么能做这么多事情。总体描述结构为:“了解脉络特征、熟悉脉络过程、分析脉络原理”三个部分。目录如下:contextGo语言的独特功能之一是什么?“有什么办法可以达到不传进去就传进去的效果吗?”听起来很神奇。在Go语言中,context是一个“一等公民”的标准库,很多开源库肯定会支持,因为标准库context的定位就是contextcontrol。会跨goroutines传播:Go语言本质上就是基于context来实现和构建各种goroutine控件,结合select-case,可以实现跨goroutines的contextdeadline、信号控制、信息传递等操作。Go语言协程的重中之重。context基本特性演示代码:funcmain(){parentCtx:=context.Background()ctx,cancel:=context.WithTimeout(parentCtx,1*time.Millisecond)defercancel()select{case<-time.After(1*time.Second):fmt.Println("overslept")case<-ctx.Done():fmt.Println(ctx.Err())}}Outputresult:contextdeadlineexceeded我们通过调用标准库context来设置parentCtx变量.WithTimeout方法设置超时时间,然后调用select-case监听context.Done方法,终于到了deadline。所以,逻辑上,select会走到context.Err的case分支,最后输出contextdeadlineexceeded。除了上述方法外,标准库上下文还支持以下方法:,CancelFunc)typeContextfuncBackground()ContextfuncTODO()ContextfuncWithValue(parentContext,key,valinterface{})ContextWithCancel:基于父上下文创建一个可以取消的新上下文。WithDeadline:基于父上下文,创建一个带有截止日期(Deadline)的新上下文。WithTimeout:基于父上下文,创建一个带有超时(Timeout)的新上下文。背景:创建一个空上下文,通常用作根父上下文。TODO:创建一个空的context,一般用于未确定时的声明式使用。WithValue:根据一个上下文创建并存储相应的上下文信息。context的本质我们在基础特性中介绍了很多context的方法,基本都是一样的。看起来并不难,让我们来看看它的底层基本原理和设计。context相关函数的标准返回值如下:funcWithXXXX(parentContext,xxxxxx)(Context,CancelFunc),返回值分别为Context和CancelFunc,我们接下来分析这两个函数。接口1.上下文接口:typeContextinterface{Deadline()(deadlinetime.Time,okbool)Done()<-chanstruct{}Err()errorValue(keyinterface{})interface{}}Deadline:获取当前上下文的截止时间。完成:获取类型为结构体的只读通道。可用于识别当前频道是否因过期或取消而关闭。Err:获取当前上下文关闭的原因。Value:获取当前上下文中存储的上下文信息。2、Canceler接口:typecancelerinterface{cancel(removeFromParentbool,errerror)Done()<-chanstruct{}}cancel:调用当前上下文的cancel方法。Done:同前,可用于识别当前通道是否已经关闭。Infrastructure在标准库上下文的设计上,一共提供了四种类型的上下文来实现上述接口。它们分别是emptyCtx、cancelCtx、timerCtx和valueCtx。在日常使用中,emptyCtx经常使用context.Background方法,或者context.TODO方法。源码如下:var(background=new(emptyCtx)todo=new(emptyCtx))funcBackground()Context{returnbackground}funcTODO()Context{returntodo}本质上是基于emptyCtx类型的基础封装。emptyCtx类型本质上实现了Context接口:)error{returnil}func(*emptyCtx)Value(keyinterface{})interface{}{returnil}其实emptyCtx类型context的实现很简单,因为是空context的定义,所以没有deadline和没有超时,可以认为是一个基本的空白上下文模板。在cancelCtx调用context.WithCancel方法的时候,我们会涉及到cancelCtx类型,它的主要功能是取消事件。源码如下:funcWithCancel(parentContext)(ctxContext,cancelCancelFunc){c:=newCancelCtx(parent)propagateCancel(parent,&c)return&c,func(){c.cancel(true,Canceled)}}funcnewCancelCtx(parentContext)cancelCtx{returncancelCtx{Context:parent中的newCancelCtx方法}}会生成一个可以取消的新上下文。如果上下文取消,其关联的子上下文和对应的协程也会收到取消信息。首先,maingoroutine创建一个新的上下文并将其传递给goroutineb。此时goroutineb的上下文是maingoroutine上下文的一个子集:在传递过程中,goroutineb将自己的上下文一一传递给goroutinec、d、e。.最后在运行时goroutineb调用cancel方法。使上下文及其对应的子集接收到取消信号,对应的goroutine也响应。接下来我们仔细看看cancelCtx类型:typecancelCtxstruct{Contextmusync.Mutex//protectsfollowingfieldsdonechanstruct{}//createdlazily,closedbyfirstcancelcallchildrenmap[canceler]struct{}//settonilbythefirstcancelcallerrerror//settonon-nilbythefirstcancel简单的,主要是children字段,里面包含上下文对应的所有子上下文,方便后续取消事件发生时一一通知关联。其他属性主要用于并发控制(互斥锁)、取消信息和错误写入:func(c*cancelCtx)Value(keyinterface{})interface{}{ifkey==&cancelCtxKey{returnc}returnc.Context。值(键)}func(c*cancelCtx)Done()<-chanstruct{}{c.mu.Lock()ifc.done==nil{c.done=make(chanstruct{})}d:=c。donec.mu.Unlock()returnd}func(c*cancelCtx)Err()error{c.mu.Lock()err:=c.errc.mu.Unlock()returnerr}你可以在上面的代码中注意到,done属性(只读通道)是在实际调用Done方法时创建的。它需要与select-case一起使用。在timerCtx调用context.WithTimeout方法的时候,我们会涉及到timerCtx类型。它的主要功能是超时和截止时间事件。源码如下:{...c:=&timerCtx{cancelCtx:newCancelCtx(parent),deadline:d,}}可以发现timerCtx类型是基于cancelCtx类型的。我们再仔细看看timerCtx结构体:typetimerCtxstruct{cancelCtxtimer*time.Timer//UndercancelCtx.mu.deadlinetime.Time}其实timerCtx类型也是cancelCtx类型,加上time.Timer和对应的Deadline,其中包括时间属性控件。让我们仔细看看它支持的取消方法,想想它是如何取消的:errerror){c.cancelCtx.cancel(false,err)ifremoveFromParent{removeChild(c.cancelCtx.Context,c)}c.mu.Lock()ifc.timer!=nil{c.timer.Stop()c.timer=nil}c.mu.Unlock()}会先调用cancelCtx类型的取消事件。如果有父节点,则移除当前上下文子节点,最后停止定时器并重置定时器。Deadline或Timeout的行为是通过timerCtx的WithDeadline方法实现的:.returnWithCancel(parent)}...}这个方法会先做一个预判。如果父节点的Deadline时间早于当前指定的Deadline时间,则直接生成cancelCtx上下文。funcWithDeadline(parentContext,dtime.Time)(Context,CancelFunc){...c:=&timerCtx{cancelCtx:newCancelCtx(parent),deadline:d,}propagateCancel(parent,c)dur:=time.Until(d)ifdur<=0{c.cancel(true,DeadlineExceeded)//deadline已经过了returnc,func(){c.cancel(false,Canceled)}}c.mu.Lock()deferc.mu.Unlock()ifc.err==nil{c.timer=time.AfterFunc(dur,func(){c.cancel(true,DeadlineExceeded)})}returnc,func(){c.cancel(true,Canceled)}}会被正式生成为一个timeCtx类型,并将其添加到父上下文是children属性。最后计算当前时间和Deadline时间,过期后调用time.AfterFunc自动调用cancel方法发起取消事件,自然会触发父子事件传播。当valueCtx调用context.WithValue方法时,我们会涉及到valueCtx类型。它的主要特点是上下文信息传递。源码如下:funcWithValue(parentContext,key,valinterface{})Context{...if!reflectlite.TypeOf(key).Comparable(){panic("keyisnotcomparable")}return&valueCtx{parent,key,val}}你会发现valueCtx结构也很简单,核心就是键值对:typevalueCtxstruct{Contextkey,valinterface{}}也就是在支持的方法上不要太复杂,基本要求是可以比较,并且然后存储和匹配:func(c*valueCtx)Value(keyinterface{})interface{}{ifc.key==key{returnc.val}returnc.Context。Value(key)}这时候你可能又会有疑惑了。多个父子上下文如何实现跨上下文的上下文信息获取?这个秘密其实也体现在上面的valueCtx和Value方法中:本质上,valueCtx类型是一个单向链表。调用Value方法时,会先检查自己的节点是否有值。如果没有,它会通过自己存储的上层父节点的信息,逐层查找对应的值,直到找到为止。在实际的工程应用中,你会发现主要的框架,比如:gin、grpc等,他都有自己的二次封装来实现一组上下文信息的传递,初衷是为了更好的管理和观察上下文信息。上下文取消事件是在我们分析了上下文的各种扩展类型和源代码之后。我们进一步提出一个问题,context是如何跨goroutine实现取消事件并传播的?它是如何实施的?这个问题的答案在于,WithCancel和WithDeadline都涉及到propagateCancel方法,其作用是构建父子上下文,如果有取消事件,会处理:funcpropagateCancel(parentContext,childcanceler){done:=parent.Done()ifdone==nil{return}select{case<-done:child.cancel(false,parent.Err())returndefault:}...}当父上下文(parent)的Done结果为nil,则直接返回,因为它不具备取消事件的基本条件,上下文可能是Background,TODO等方法生成的空上下文。当父上下文(parent)的Done结果不为nil时,发现父上下文已经取消。作为其子上下文,上下文将触发取消事件并返回父上下文的取消原因。funcpropagateCancel(parentContext,childcanceler){...ifp,ok:=parentCancelCtx(parent);ok{p.mu.Lock()ifp.err!=nil{child.cancel(false,p.err)}else{ifp.children==nil{p.children=make(map[canceler]struct{})}p.children[children]=struct{}{}}p.mu.Unlock()}else{atomic.AddInt32(&goroutines,+1)gofunc(){select{case<-parent.Done():child.cancel(false,parent.Err())case<-child.Done():}}()}}通过前面的代码片段根据判断可知父上下文没有触发取消事件,当前父子上下文正常(未取消)。将执行以下过程:调用parentCancelCtx方法找到具有取消功能的父上下文。将当前上下文,即child添加到父上下文的children列表中,等待后续父上下文的取消事件通知和响应。如果没有找到parentCancelCtx方法,就会启动一个新的goroutine来监听父子上下文的取消事件通知。通过上下文取消事件和整体源码分析可知,cancelCtx类型的上下文包含其下属的所有子节点信息:即在map[canceler]struct{}中已经支持子节点信息子属性的存储结构。为了找到超层关系,自然要传播取消事件。具体取消事件的实际行为在上面提到的propagateCancel方法中。例如在执行cacenl方法时,会分别判断父子上下文的状态。如果满足,该事件将被取消并传播给孩子们。同步已取消。总结作为Go语言的核心功能之一,标准库上下文其实非常简短,使用了基本的数据结构和概念。它不仅满足跨goroutines的调控,如并发、超时控制等,同时还满足context的信息传递。在工程应用中,如链接ID、公开参数、鉴权验证等,都会使用上下文作为媒介。目前官方对于context的建议是作为方法的第一个参数传入。虽然有点麻烦,但还是有人选择在结构体中作为属性传入。但这也会带来一些精神负担,需要辨别是否要创造一个新的。也有人希望Go2取消上下文,换成另一种方法,但总的来说,目前还没有看到正式的提议。这是我们都需要重新思考的问题。
