世界时讯:[Golang]正确使用Context
2023-03-15 15:09:52 来源:腾讯云
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之间传递。
标签:
相关阅读
- (2023-03-15)世界时讯:[Golang]正确使用Context
- (2023-03-15)热点聚焦:*ST荣华因未及时披露公司重大事件等违规行为被上海证券交易所通报批评
- (2023-03-15)微视里面怎么删作品-微视怎么删除自己的作品
- (2023-03-15)广元市委书记何树平:把抓项目强产业作为拼经济比发展的重要抓手
- (2023-03-15)烽皇瑞根小说女主_烽皇瑞根 焦点日报
- (2023-03-15)倒挂是什么意思网络用语_倒挂是什么意思|当前讯息
热点推荐
- (2023-03-15)世界时讯:[Golang]正确使用Context
- (2023-03-15)快资讯丨为棚户区群众圆“安居梦”
- (2023-03-15)做好居民“点滴小事”就是社区治理的大事 快消息
- (2023-03-15)【新要闻】强化“三个要素”保障 助力营商环境升级
- (2023-03-15)吉林抚松万良司法所开展3·15国际消费者权益日普法宣传活动_天天播资讯
- (2023-03-15)【全球播资讯】山东泰安市泰山区开辟项目环评“绿色通道”
- (2023-03-15)量身定制话术陷阱、虚增电影制作成本等 六类新消费陷阱更容易让人上当
- (2023-03-15)收定金后突然失联等上门维修的坑谁来填?房主失联骗局了解一下
- (2023-03-15)热点评!西子洁能:德海艾科交付的公司崇贤工厂兆瓦级项目运行顺利
- (2023-03-15)盾安环境:公司北美客户的开拓正在有序开展中
- (2023-03-15)天天最新:广电运通:中新广州知识城产业园“新一代AI智能产业基地”一期预计2023年年中建成投产
- (2023-03-15)前沿生物:公司已商业化产品艾可宁为多肽类药物_环球报资讯
- (2023-03-15)徐海乔承认过的女友 徐海乔个人简介
- (2023-03-15)顾客曝沃尔玛超市盒饭上蟑螂乱爬 沃尔玛老板是谁?
- (2023-03-15)女子提前还房贷被要求买10万理财 50万是提前还房贷还是买理财?
- (2023-03-15)【全球快播报】海天瑞声:ChatGPT代表的大模型带来AI产业新变革,将持续、密切关注发展趋势
- (2023-03-15)热点聚焦:*ST荣华因未及时披露公司重大事件等违规行为被上海证券交易所通报批评
- (2023-03-15)金螳螂:螳螂家装目前正在积极做转型升级 未来会在智能家装等多领域进行重点布局
- (2023-03-15)亨通光电:目前公司的电缆产品尚未用于国产大飞机
- (2023-03-15)国家统计局:1-2月规模以上工业增加值同比增2.4%
- (2023-03-15)北陆药业:九味镇心颗粒为国家医保乙类药品 滚动
- (2023-03-15)天天看热讯:前2月房地产开发投资同比降5.7% 住宅销售额增3.5%
- (2023-03-15)钧达股份:预计2023年底公司将拥有N型产能31GW_全球新视野
- (2023-03-15)腾亚精工:公司在国内率先实现自主品牌燃气射钉枪量产 焦点播报
- (2023-03-15)南京最齐最全的电商展,3月22-24日盛大开幕!先睹为快!附观展指南
- (2023-03-15)网文连载十余年被网友举报 《校花的贴身高手》好看吗?
- (2023-03-15)板其站_关于板其站简述-世界信息
- (2023-03-15)花园生物:花园药业持有心脑健片等四项中成药的药品批准文号_独家焦点
- (2023-03-15)环球焦点!国药太极产地加工垫高原材料质量厚度
- (2023-03-15)全球看点:浩物股份:子公司内江市鹏翔投资旗下4S店主要分布在天津
- (2023-03-15)坚守“金融为民”本色 擦亮“客户中心”招牌 上海农商银行打造全方位多元化宣教体系
- (2023-03-15)天天速读:百纳千成:公司于3月13日宣布成为百度文心一言首批生态合作伙伴
- (2023-03-15)清明节放假一天不调休 为什么今年清明节不调休?
- (2023-03-15)微视里面怎么删作品-微视怎么删除自己的作品
- (2023-03-15)记者暗访日租女友行业 合法性和合理性都值得考虑
- (2023-03-15)信用卡逾期还款后卡丢了怎么办?信用卡逾期8天还款补救办法
- (2023-03-15)最资讯丨欧佩克下调2023年全球原油需求预测
- (2023-03-15)【天天热闻】原油跌至三个月来低点,因通胀以及美国银行业担忧
- (2023-03-15)环球焦点!哈萨克斯坦能源部长确认4月再向德国出口石油2万吨
- (2023-03-15)陕西榆林首个面向社会经营的成品油仓储库项目开工
- (2023-03-15)沙特阿拉伯石油巨头2022年净利润创历史新高
- (2023-03-15)天润工业:公司与重卡行业相关性紧密 产品市场占有率高|全球热讯
- (2023-03-15)南方航空:南航物流已提交了上市辅导备案申请材料
- (2023-03-15)凤凰新媒体2022年第四季度财报高管解读
- (2023-03-15)看热讯:科创更前,上海农商银行携手科技企业孵化器 全力支持科创企业发展
- (2023-03-15)每日关注!东方雨虹:选择趋势,野蛮生长
- (2023-03-15)Juniper Research:到2030年全球CBDC交易额将超过2130亿美元
- (2023-03-15)硅谷银行暴雷,联邦存款保险公司如何兜底
- (2023-03-15)硅谷银行“猝死”,恐慌蔓延加密资产?_消息
- (2023-03-15)癌症病人信用卡逾期怎么办?癌症患者刷爆信用卡怎么办?
- (2023-03-15)7张信用卡欠5万没逾期怎么办?欠7万信用卡逾期了怎么办?
- (2023-03-15)远行路上,总有中荷人寿的服务保驾护航
- (2023-03-15)gtx 970(gtx970)|全球百事通
- (2023-03-15)广元市委书记何树平:把抓项目强产业作为拼经济比发展的重要抓手
- (2023-03-15)世界即时看!泓淋电力将强势登陆创业板 行业中的高质量发展“全科绩优生”
- (2023-03-15)龙洲股份:控股子公司中汽宏远具备纯电动物流车的生产和销售资质
- (2023-03-15)全国高中C9联盟成立 形成可持续的示范效应
- (2023-03-15)多家电商平台有女性迷药隐蔽出售 “这是极其恶劣的公然犯罪”
- (2023-03-15)寒武纪碾坝
- (2023-03-15)烽皇瑞根小说女主_烽皇瑞根 焦点日报
- (2023-03-15)侠盗猎车手_说一说侠盗猎车手的简介-世界动态
- (2023-03-15)倒挂是什么意思网络用语_倒挂是什么意思|当前讯息
- (2023-03-14)青岛科技大学专科_青岛科技大学专科分数线_世界热推荐
- (2023-03-14)2023年成都凤凰湖灯会区域樱花节期间免费开放 每日速讯
- (2023-03-14)盈建科:公司相关产品可以帮助行业客户对AutoCAD等国外软件进行国产替代_热点聚焦
- (2023-03-14)国信证券:公司及子公司不存在与美国硅谷银行、富国银行开展业务合作或资金存储的情况 聚看点
- (2023-03-14)和而泰:公司会根据客户的订单需求及时交付 同时也会积极拓展新客户和新项目合作
- (2023-03-14)速递!易瑞生物:公司新冠流感病毒甲型流感病毒乙型流感病毒抗原等多组合联合检测试剂产品已完成欧盟的准入流程
- (2023-03-14)【世界热闻】欣贺股份:目前展开的一期合作实现了从0到1的过程 总体达到预期
- (2023-03-14)华阳集团:公司深耕汽车产业多年 整车企业对零部件供应商基本每年都有降价要求
- (2023-03-14)京东超市发布四项高增长 超大盘数据:下沉用户2亿 品牌会员过亿 PLUS消费用户3千万 环球新动态
- (2023-03-14)铜价可能面临潜在的极端上行风险,中国从熊市因素转为牛市因素
- (2023-03-14)西部牧业:公司拥有6家参股奶牛养殖公司 牧草均从周边地区收购
- (2023-03-14)环球微动态丨科信技术:本次发行的定价基准日为发行期首日
- (2023-03-14)“口袋公园”添绿意 城市美景入画来
- (2023-03-14)全球快讯:四川青川青年干部“争先提能”
- (2023-03-14)以高质量就业助力县域经济高质量发展|天天即时
- (2023-03-14)候诊区开设微讲堂
- (2023-03-14)中国上市公司ESG创新联盟与中国上市公司ESG智库在成都温江正式成立
- (2023-03-14)英媒:大多数南极动植物的数量到2100年时会减少-环球观热点
- (2023-03-14)阳光诺和2022年归母净利同增近50% “CRO+CDMO”协同布局成效将显 最新资讯
- (2023-03-14)陇南何以能种茶?_今日视点
- (2023-03-14)两会两个“3%”逐步释放消费潜力,长期利好大食品种业上市公司
- (2023-03-14)聚焦IPO | 拉拉米重要股东陷债务危机,业绩依赖头部品牌,存续约之忧 环球今热点
- (2023-03-14)即时看!万达信息:公司将积极申请公共数据的加工使用权 形成可上市交易的数据资产
- (2023-03-14)兰州银行:截至目前分支机构共计173家 覆盖甘肃全省 当前短讯
- (2023-03-14)全球热文:博腾股份:公司及子公司未在硅谷银行开立账户及存款
- (2023-03-14)新和成:公司的有息负债主要为保障生产经营和项目建设所需|天天新视野
- (2023-03-14)东方通:公司主要为三大运营商提供基础软件、信息安全、网络安全、数据安全等 速讯
- (2023-03-14)籍的意思_籍此是什么意思
- (2023-03-14)第四届木棉论坛揭幕:张兰、陈灵梅、崔军红、董宁等女企业家热议产业经济“她势力”
- (2023-03-14)世界今头条!打印标签用什么app(打印标签用什么软件)
- (2023-03-14)每日看点!华润三九:华润英特项目目前在推进中 尚未正式投产
- (2023-03-14)华泰证券联合主办的“从城市到荒野”生物多样性主题展在南京开幕
- (2023-03-14)全球最新:双枪科技:近期公司团队已赴美参加2023年美国芝加哥国际家庭用品博览会
- (2023-03-14)世界热门:克明食品:公司有专门针对儿童群体研发生产不添加食用盐的儿童面
- (2023-03-14)环球热讯:山西证券:公司及子公司没有在硅谷银行及富国银行开户 也没有资金储蓄和业务合作
- (2023-03-14)全球要闻:产业互联网,一个预先写就的故事范本
- (2023-03-14)哪个银行的网银最好用?四大行谁最靠谱?
- (2023-03-14)当前观点:宝泰隆:公司共有8座煤矿 已全部取得采矿权证