全球速递![Golang]正确使用Context
01 为什么要引入Context
context.Context是Go中定义的一个接口类型,从1.7版本中开始引入。其主要作用是在一次请求经过的所有协程或函数间传递取消信号及共享数据,以达到父协程对子协程的管理和控制的目的。
需要注意的是context.Context的作用范围是一次请求的生命周期,即随着请求的产生而产生,随着本次请求的结束而结束。如图所示:
02 什么是context.Context
在context包中,我们看到context.Context的定义实际上是一个接口类型,该接口定义了获取上下文的Deadline的函数,根据key获取value值的函数、还有获取done通道的函数。如下:
【资料图】
typeContextinterface{Deadline()(deadlinetime.Time,okbool)Done()<-chanstruct{}Err()error Value(key interface{}) interface{}}
由定义的接口函数可知,对于传递取消信号的行为我们可以描述为:当协程运行时间达到Deadline时,就会调用取消函数,关闭done通道,往done通道中输入一个空结构体消息struct{}{},这时所有监听done通道的子协程都会收到该消息,便知道父协程已经关闭,需要自己也结束运行。
下面是一个使用Context的简易示例,我们通过该示例来说明父子协程之间是如何传递取消信号的。
func main() { ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) defer cancel() go doSomethingCool(ctx) select { case <-ctx.Done(): fmt.Println("oh no, I"ve exceeded the deadline") }}func doSomethingCool(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("timed out") return default: fmt.Println("doing something cool") } time.Sleep(500 * time.Millisecond) }}
由示例可知,main协程和doSomething函数之间的唯一关联就是ctx.Done()。当子协程从ctx.Done()通道中接收到输出时(因为超时自动取消或主动调用了cancel函数),即认为是父协程不再需要子协程返回的结果了,子协程就会直接返回,不再执行其他的逻辑。
03 Context的作用一:协程间传递信号
3.1 如何创建带可以传递信号的Context
在开头处我们得知Context本质是一个接口类型。接口类型是需要具体的结构体起来实现的。那我们需要自定义结构体类型来实现这些接口吗?答案是不需要。因为在context包中已经定义好了所需场景的结构体,这些结构体已经帮我们实现了Context接口的方法,在项目中就已经够用了。
在context包中定义有emptyCtx、cancelCtx、timerCtx、valueCtx
四种结构体。其中cancelCtx、timerCtx实现了给子协程传递取消信号。valueCtx结构体实现了父协程和子协程传递共享数据相关。本节我们重点来看跟传递信号相关的Context。
在上面示例中,我们通过context.WithTimeout函数创建了一个带定时取消功能的Context实例,该示例本质上是创建了一个timerCtx结构体的实例。在context包中还有WithCancel、WithDeadline函数也可以创建对应的结构体,其定义如下:
//创建带有取消功能的Contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc) //创建带有定时自动取消功能的Contextfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)//创建带有定时自动取消功能的Contextfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
对应的函数创建的结构体及该实例所实现的功能的主要特点如下图所示:
在图中我们看到结构体依次是继承关系。因为在cancelCtx结构体内嵌套了Context(实际上是emptyCtx)、timerCtx结构体内嵌套了cancelCtx结构体,可以认为他们之间存在继承关系。
通过WithTimeout和WithDealine函数创建的Context实际上都是timerCtx结构体,唯一的区别就是WithDeadline函数的第二个参数指定的是最后的时间点,而WithTimeout函数的第二个参数是一段时间。但WithDealine在内部实现中本质上也是将时间点转换成距离当前的时间段。
3.2 为什么Done函数返回值是通道
在Context接口的定义中我们看到Done函数的定义,其返回值是一个输出通道:
Done() <-chan struct{}
在上面的示例中我们看到的子协程是通过监听Context的Done()函数返回的通道来判断父协程是否发送了取消信号的。当父协程调用取消函数时,该取消函数将该通道关闭。关闭通道相当于是一个广播信息,当监听该通道的接收者从通道到中接收完最后一个元素后,接收者都会解除阻塞,并从通道中接收到通道元素类型的零值。
既然父子协程是通过通道传到信号的。下面我们介绍父协程是如何将信号通过通道传递给子协程的。
3.3 父协程是如何取消子协程的
我们发现在Context接口中并没有定义Cancel方法。实际上通过WithCancel函数创建的一个具有可取消功能的Context实例来实现的:
// WithCancel returns a copy of parent whose Done channel is closed as soon as// parent.Done is closed or cancel is called.func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }}
WithCancel函数的返回值有两个,一个是ctx,一个是取消函数cancel。当父协程调用cancel函数时,就相当于触发了关闭的动作,在cancel的执行逻辑中会将ctx的done通道关闭,然后所有监听该通道的子协程就会收到一个struct{}类型的零值,子协程根据此便执行了返回操作。下面是cancel函数实现:
// cancel closes c.done, cancels each of c"s children, and, if// removeFromParent is true, removes c from its parent"s children.func (c *cancelCtx) cancel(removeFromParent bool, err error) { //... d, _ := c.done.Load().(chan struct{})//获取通道 if d == nil { c.done.Store(closedchan) } else { close(d) //关闭通道done } //...}
由源码可知,cancelCtx的cancel函数执行时会关闭通道close(d)。
通过WithCancel函数构造的Context,需要开发者自己设定调用取消函数的条件。而在某些场景下需要设定超时时间,比如调用grpc服务时设置超时时间,那么实际上就是在构造Context的同时,启动一个定时任务,当达到设定的定时时间时,就自动调用cancel函数即可。这就是context包中提供的WithDeadline和WithTimeout函数来构造的上下文。如下是WithDeadline函数的关键实现部分:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { //... c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) //... if c.err == nil { //这里实现定时器,即dur时间后执行cancel函数 c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}
WithTimeout函数也是将相对时间timeout转换成绝对的时间点deadline之后,调用的WithDeadline函数。
3.4为什么要通过WithXXX函数构造一个树形结构
很多文章都说,通过WithXXX函数基于Context会衍生出一个Context树,树的每个节点都可以有任意多个子节点Context。如下图表示:
那为什么要构造一个树形结构呢?我们从处理一个请求时经过的多个协程来角度来理解会更容易一些。当一个请求到来时,该请求会经过很多个协程的处理,而这些协程之间的关系实际上就组成了一个树形结构。如下图:
Context的目的就是为了在关联的协程间传递信号和共享数据的,而每个协程又只能管理自己的子节点,而不能管理父节点。所以,在整个处理过程中,Context自然就衍生成了树形结构。
3.5为什么WithXXX函数返回的是一个新的Context对象
通过WithXXX的源码可以看到,每个衍生函数返回来的都是一个新的Context对象,并且都是基于parent Context的。以WithDeadline为例,就是返回的一个timerCtx新的结构体实例。这是因为,在Context的传递过程中,每个协程都能根据自己的需要来定制Context(例如,在上图中,main协程调用goroutine2时要求是600毫秒完成操作,但goroutine2调用goroutine2.1时,要求是500毫秒内完成操作),而这些修改又不能影响之前已经调用的函数,只能对向下传递。所以,通过一个新的Context值来进行传递。
04 Context的作用二:协程间共享数据
Context的另外一个功能就是在协程间共享数据。该功能是通过WithValue函数构造的Context来实现的。我们看下WithValue的实现:
func WithValue(parent Context, key, val interface{}) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val}}
实现代码很简短,我们看到最终返回的是一个valueCtx结构体实例。其中有两点:一是key的类型必须是可比较的。二是value是不能修改的,即具有不可变性。如果需要添加新的值,只能通过WithValue基于原有的Context再生成一个新的valueCtx来携带新的key-value。这也是Context的值在传递过程中是并发安全的原因。从另外一个角度来说,在获取一个key的值的时候,也是递归的一层一层的从下往上查找,如下:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
上面简单介绍了下在协程间调用的时候是如何通过Context共享数据的。
但这里讨论的重点是什么样的数据需要通过Context来共享,而不是通过传参的方式。总结下来有以下两点:
携带的数据作用域必须是在请求范围内有效的。即该数据随着请求的产生而产生,随着请求的结束而结束,不会永久的保存。携带的数据不建议是关键参数,关键参数应显式的通过参数来传递。例如像trace_id之类的,用于维护作用,就适合用在Context中传递。4.1 什么是请求范围(request-scoped)内的数据
这个没有一个明显的划定标准。一般的请求范围的数据就是用来表示该请求的元数据。比如该请求是由谁发出(即user id),该请求是在哪儿发出的(即user ip,请求是从该用户的ip位置发出的)。
例如,如果一个日志对象logger是一个单例那么它也不是一个请求范围内的数据。但如果该logger包含了发送请求的来源信息,以及该请求是否启动了调试功能的开关信息,那么该logger也可以被认为是一个请求范围内的数据。
4.2 使用Context.Value的缺点
使用Context.Value会对降低函数的可读性和表达性。例如,下面是使用Context.Value来携带token验证角色的示例:
func IsAdminUser(ctx context.Context) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot()}
当用户调用该函数的时候,仅仅知道该函数带有一个Context类型的参数。但如果要判断一个用户是否是Admin必须要两部分要说明:一个是验证过的token,一个是认证服务。
我们将该函数的Context移除,然后使用参数的方式来重构,如下:
func IsAdminUser(token string, authService AuthService) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot()}
那么这个函数的可读性和表达性就比重构前提高了很多。调用者通过函数签名就很容易知道要判断一个用户是否是AdminUser,只需要传入token和认证的服务authService即可。
4.3 context.Value的使用场景
一般复杂的项目都会有中间件层以及大量的抽象层。如果将类似token或userid这样简单的参数以参数的方式从第一个函数层层传递,那对调用者来说将会是一种噩梦。如果将这样的元数据通过Context来携带进行传递,将会是比较好的方式。在实际项目中,最常用的就是在中间件中。我们以iris为web框架,来看下在中间件中的应用:
package mainimport ( "context" "github.com/google/uuid" "github.com/kataras/iris/v12")func main() { app := iris.New() app.Use(RequestIDMiddleware) app.Get("/hello", mainHandler) app.Listen("localhost:8080", iris.WithOptimizations)}func RequestIDMiddleware(c iris.Context) { reqID := uuid.New() ctx := context.WithValue(c.Request().Context(), "req_id", reqID) req := c.Request().Clone(ctx) c.ResetRequest(req) c.Next()}func mainHandler(ctx iris.Context) { req_id := ctx.Request().Context().Value("req_id") ctx.Writef("Hello request id:%s", req_id) return}
05 总结
context包是go语言中的一个重要的特性。要想正确的在项目中使用context,理解其背后的工作机制以及设计意图是非常重要的。context包定义了一个API,它提供对截止日期、取消信号和请求范围值的支持,这些值可以跨API以及在Goroutine之间传递。
标签:
-
2022-09-15 14:23:06
杨莉娜租借加盟巴黎圣日耳曼 与姆巴佩梅西拉莫斯一起共事<
北京时间9月13日下午,法甲女足俱乐部巴黎圣日耳曼正式官宣中国女足国脚杨莉娜完成租借加盟,合同期至2023年6月。杨莉娜成为目前国家队中第
-
2022-02-07 14:57:45
奇迹!绝杀!女足亚洲杯逆转夺冠!<
刚刚,中国女足上演逆转绝杀奇迹!她们在亚洲杯决赛中3:2力克韩国队,时隔16年再夺亚洲杯冠军!
-
2022-02-07 14:57:45
中国政府与阿根廷共和国政府签署共建“一带一路”谅解备忘录<
新华社北京2月6日电(记者安蓓)国家发展改革委6日称,国家发展改革委主任何立峰与阿根廷外交、国际贸易和宗教事
-
2022-02-07 14:57:43
中华人民共和国和阿根廷共和国关于深化中阿全面战略伙伴关系的联合声明(全文)<
新华社北京2月6日电中华人民共和国和阿根廷共和国关于深化中阿全面战略伙伴关系的联合声明一、应中方邀请,阿根廷
-
2022-02-07 14:57:40
春节假期国内旅游出游2.51亿人次<
春节遇冬奥,旅游年味浓。根据文化和旅游部数据中心测算,2022年春节假期7天,全国国内旅游出游2 51亿人次,同比
-
2023-03-16 02:08:53
全球速递![Golang]正确使用Context
context Context是Go中定义的一个接口类型,从1 7版本中开始引入。其主要作用是在一次请求经过的所有协程或函数间传递取消信号及共享数据,以
-
2023-03-15 22:16:46
头嗡嗡响是什么原因引起的怎么治疗_头嗡嗡响是什么原因
1、鸣或脑鸣如果发生在年轻人,特别是学生患者,多数是由于紧张压力等精神因素引起的,属于功能性疾病,病人多数有焦虑症状,烦
-
2023-03-15 19:21:24
长春机场积极应对雨雪天气航班保障
长春机场积极应对雨雪天气航班保障
-
2023-03-15 17:05:35
英雄联盟蛮王出装顺序(英雄联盟蛮王出装) 最新资讯
1、电刀,无尽,破碎,饮血,复活甲,银丝带。2、电刀提高单带能力,电刀附带的弹射伤害提高清线速度和效率;无尽提升暴击伤害
-
2023-03-15 15:05:26
高速上轿车撞进卡车底部 两人被困 当前速递
高速上轿车撞进卡车底部两人被困
-
2023-03-15 12:54:19
筑梦“新”生活 马上消费获《中国银行保险报》2022年度“服务新市民创新案例” 环球看点
3月14日,《中国银行保险报》在北京举办2023年中国银行业保险业服务创新峰会,主题为“创新金融服务共创美好生活”。凭借着在新市民领域的...
-
2023-03-15 11:03:50
妖狐x仆ss第二季动漫全集_狐妖x仆ss第二季 天天通讯
1、妖狐x仆ss没有第二季(截至2020年8月22日)电视动画《妖狐×仆SS》改编自日本漫画家藤原可可亚原作的同名漫画。
-
2023-03-15 08:52:39
北京:1-2月一般公共预算支出1561.5亿元,增长6.5% 今日聚焦
北京市2023年1-2月财政收支情况一般公共预算收入情况2月以来,随着经济持续恢复,全市一般公共预算收入增速企稳回升,实
-
2023-03-15 05:30:24
金科服务(09666.HK):3月14日南向资金增持21.29万股 每日速看
3月14日北向资金增持21 29万股金科服务(09666 HK)。近5个交易日中,获南向资金增持的有3天,累计净增持33 03万股。近20个交易日中,获南向资
-
2023-03-15 00:46:12
生命之果——无花果,坚持食用,身体会收获哪些好处?_环球热资讯
印象中,大多数植物都会先开花,然后再结果,这已经是自然界不成文的规定。但是有一种植物很特别,它就是无花果。单听名字就知道,这是一种不
-
2023-03-14 21:00:28
花开有时颓靡无声
1、《花开有时。2、颓靡无声》是水千丞于2014年在晋江文学城写的耽美小说。3、已出版繁体。以上就是【花开有时颓靡无
-
2023-03-14 18:18:37
西媒:利雅得新月3亿欧邀梅西加盟,梅西父亲要6亿&收入超C罗 环球百事通
西媒:利雅得新月3亿欧邀梅西加盟,梅西父亲要6亿&收入超C罗,c罗,新月,西媒,利雅得,里奥梅西,弗拉门戈,德国足球,皇家马德里,足球运动员,阿根廷
-
2023-03-14 16:07:59
063期老梁排列三预测奖号:第三位温码回暖
开奖回顾:排列三第2023062期奖号为:006,各位号码遗漏值分别为17、5、25,遗漏总值为47。上期第一位开出奖号:0,间隔17期出现,走势非常冷,最近1
-
2023-03-14 14:18:33
总资产报酬率合理范围(总资产报酬率的范围)
总资产报酬率合理范围,总资产报酬率的范围这个很多人还不知道,现在让我们一起来看看吧!1、当然高的好,相当于你的每1元资产为你带来的报酬,
-
2023-03-14 11:56:46
公司回应员工给客户倒水太满被开除具体详细内容是什么
公司回应员工给客户倒水太满被开除今天的热度非常高,现在也是在热搜榜上了,那么具体的公司回应员工给客户倒水太满被开除是什么
-
2023-03-14 10:01:37
SIPRI:乌克兰去年武器进口全球第三,仅次于卡塔尔与印度
【环球时报综合报道】当地时间13日,斯德哥尔摩国际和平研究所(SIPRI)最新报告称,美国依旧是全球最大武器出口国,第二至第五分别为俄罗斯、
-
2023-03-14 07:12:09
吉鲁本场数据:射门4脚打进1球,赢得4次空中拼抢
吉鲁本场数据:射门4脚打进1球,赢得4次空中拼抢,吉鲁本,骑士团,波兰足球,英国足球,奥利弗·吉鲁,国际足球赛事
-
2023-03-14 01:00:53
前裁判:卡塞米罗没学会调整比赛方式,希望滕哈赫能给他建议_新视野
前裁判:卡塞米罗没学会调整比赛方式,希望滕哈赫能给他建议,英超,基恩,波尔,滕哈赫,维埃拉,前裁判,卡塞米罗,卡斯米路,足球竞赛,足球运动员,葡
-
2023-03-13 20:59:20
魔兽怀旧荣誉和军衔经验换算_魔兽世界荣誉军衔 天天观察
1、等级联盟头衔部落头衔奖励14大元帅高阶督军史诗品质的武器和盾牌13元帅督军史诗品质的头盔、护
-
2023-03-13 17:56:19
有色50ETF: 汇添富基金管理股份有限公司关于汇添富中证细分有色金属产业主题交易型开放式指数证券投资基金增加申购赎回代理券商的公告-世界热头条
有色50ETF:汇添富基金管理股份有限公司关于汇添富中证细分有色金属产业主题交易型开放式指数证券投资基金增加申购赎回代理券商的公告
-
2023-03-13 15:53:37
圣元环保(300867)3月13日主力资金净卖出1175.14万元 最新快讯
截至2023年3月13日收盘,圣元环保(300867)报收于18 72元,下跌2 65%,换手率2 33%,成交量3 57万手,成交额6700 74万元。
-
2023-03-13 13:54:16
男子酒驾先闯卡后换座,监控之下现原形_全球快消息
日前,@宿州公安交警在线三大队开展酒驾整治时,发现一辆小车行迹可疑,在要求其接受检查时,驾驶员强行闯卡并让副驾驶坐到主驾驶位,而后迅速
-
2023-03-13 11:05:04
高盛:鉴于最近银行体系承受的压力 不再预期美联储将在3月22日的会议上加息 全球热消息
【高盛:鉴于最近银行体系承受的压力不再预期美联储将在3月22日的会议上加息】财联社3月13日电,高盛研究报告称,鉴于最近银行体系承受的压力
-
2023-03-13 09:14:32
焦点观察:券商观点|钢铁行业周报:供需基本面持续向好,钢厂利润修复加快
3月12日,华福证券发布一篇钢铁行业的研究报告,报告指出,供需基本面持续向好,钢厂利润修复加快。 报告具体内容如下: 投资策略:本周
-
2023-03-13 05:07:10
生死相依演员表全部(电视剧的演职员名单) 全球热头条
生死相依演员表全部,电视剧的演职员名单很多人还不知道,现在让我们一起来看看吧!解答:1、亦儒饰兰卓、(童年)、姜(成年)饰梁素素、饰沈睿、
-
2023-03-12 23:12:42
特罗萨德助攻戴帽,记者:他不可阻挡,对方球员都近不了身_今日快讯
特罗萨德助攻戴帽,记者:他不可阻挡,对方球员都近不了身,英超,阿森纳,不可阻挡,富勒姆队,英国足球,足球竞赛,比利时足球,国际足球赛事,莱昂德
-
2023-03-12 18:51:31
魔幻手机的魔幻之旅全本下载_魔幻手机的魔幻之旅 每日观点
1、什么东东!这不是电视剧吗这是电视剧啊?什么免费的?。本文分享完毕,希望对大家有所帮助。
-
2023-03-12 15:10:35
韩国3场小组赛进6球0丢球,U20国足要如何击败... 信息
韩国3场小组赛进6球0丢球,U20国足要如何击败强敌韩国进世青赛?韩国U20小组赛2胜1平积7分,C组第一晋级,打入6球0丢球!韩国是本届杯赛目前唯
-
2023-03-12 11:07:46
新乡平原大学在河南排名_新乡平原大学-焦点资讯
1、河师大平原湖校区是本科校区。2、之前是河南师范大学新联学院新乡校区。3、从今年开始这个校区就不再以新联学院的名义招生
-
2023-03-12 07:43:08
全国政协委员宋朝学:在成渝地区双城经济圈先行先试税收优惠政策
全国政协委员宋朝学:在成渝地区双城经济圈先行先试税收优惠政策,“建议在成渝地区双城经济圈先行先试税收优惠政策。”2023年全国两会正在...
-
全球速递![Golang]正确使用Context
2023-03-16 02:08:53 -
头嗡嗡响是什么原因引起的怎么治疗_头嗡嗡响是什么原因
2023-03-15 22:16:46 -
长春机场积极应对雨雪天气航班保障
2023-03-15 19:21:24 -
英雄联盟蛮王出装顺序(英雄联盟蛮王出装) 最新资讯
2023-03-15 17:05:35 -
高速上轿车撞进卡车底部 两人被困 当前速递
2023-03-15 15:05:26 -
筑梦“新”生活 马上消费获《中国银行保险报》2022年度“服务新市民创新案例” 环球看点
2023-03-15 12:54:19 -
妖狐x仆ss第二季动漫全集_狐妖x仆ss第二季 天天通讯
2023-03-15 11:03:50