首页 > 电视 > > 正文

天天简讯:[Golang]正确使用Context

2023-03-16 08:05:08 来源:腾讯云

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之间传递。

标签:

天天简讯:[Golang]正确使用Context

context Context是Go中定义的一个接口类型,从1 7版本中开始引入。其主要作用是在一次请求经过的所有...

2023-03-16 08:05:08

广播稿大全集(广播稿大全)

1、励志的广播稿‎是男儿总要走向远方,‎走向远方是为了让生‎命更辉煌。2、‎走在崎岖不平的路上,年...

2023-03-16 04:08:36

后世界末日冒险游戏高水加入Netflix_环球热点

Netflix在其不断增长的可用游戏列表中添加了另一款手机游戏,Highwater将其后世界末日的冒险故事带到了...

2023-03-15 23:16:36

世界聚焦:持有期债基带来投资新视角 鹏华固收名将刘涛打造高水准产品线

持续创新迭代的公募市场,为投资者提供了丰富多样的产品选择。在不确定态势下,能够创造长期可持续的稳...

2023-03-15 20:14:02

环球观速讯丨大东南(002263):全面预算管理制度(2023年3月)

浙江大东南股份有限公司全面预算管理制度浙江大东南股份有限公司全面预算管理制度第一条为优化浙江大东...

2023-03-15 18:09:18

每日快看:肝炎的传染途径_丙肝传播途径

1、丙肝的传播途径?1 经血液传播:血液传播是丙肝病毒最主要的传播途径,其中包括两个方面:  ①输被...

2023-03-15 15:54:11

61岁创造历史的杨紫琼,她赢的不只是奥斯卡影后_环球动态

而《瞬息全宇宙》里拯救世界的超级英雄,竟然是一位平凡年长亚洲移民女性,是我们每天都会与擦肩而过却...

2023-03-15 14:03:25

婉仪氦检漏仪(婉仪) 环球速讯

1、1 婉仪,美好的仪态。2、明张居正《来雁说》:“矧其耿特之禽,婉仪敛翮,引翁骈蹼,邕邕肃肃,似扰...

2023-03-15 11:06:53

焦点滚动:营业收入突破千亿!安徽规上数字创意企业达806家

营业收入突破千亿!安徽规上数字创意企业达806家

2023-03-15 09:58:03

世界热消息:软件百科书_软件百科

1、神机妙算造价软件是一款专业的造价软件,能够在多个行业当中帮助用户在造价上面提供相关的帮助,让造...

2023-03-15 06:56:52

主题出版研究著作《高度与温度》出版 当前热文

本报讯2月27日,主题出版学术研讨会暨《高度与温度:主题出版研究导论》出版座谈会在京举行,中国出版协...

2023-03-15 02:28:30

赛特新材股东户数增加5.81%,户均持股79.56万元-环球快资讯

赛特新材最新股东户数4589户,低于行业平均水平。公司户均持有流通股份1 74万股;户均流通市值79 56万元。

2023-03-14 21:57:49

东方银星证券简称自3月20日起变更为“庚星股份”

东方银星(600753 SH)发布公告,经公司申请,并经上海证券交易所办理,公司证券简称自2023年3月20日由“...

2023-03-14 18:56:36

深圳光明区红坳公租房认租申请条件2023 天天关注

光明区面向特殊群公供应房源46套,网上认租时间为:2023年3月14日9:00至2023年3月20日18:00,认租申请条...

2023-03-14 16:46:18

全球观速讯丨iPhone17Pro被曝:首次搭载屏下FaceID以及屏下前置摄像头,干掉“灵动岛”打孔屏,渲染图曝光

今年的iPhone15系列虽然还要半年后才会发布,其实秘密已经不多,外观是全系灵动岛打孔屏。苹果对iPhone...

2023-03-14 15:06:27

天蝎女的渣不渣_天蝎座女渣不渣|全球热闻

1、天蝎男是真的渣,但也只是针对某一部分人而言。2、因为天蝎男的容易到处跟女生暧昧,跟中央空调一样...

2023-03-14 12:51:03

上海建科上交所上市 力争成为建设行业科技创新引领者 全球讯息

东方网通讯员董敏琴3月14日报道:上海建科集团股份有限公司首次公开发行股票(股票简称:上海建科股票代...

2023-03-14 10:07:46

预计旱涝灾害较上年偏重发生 云南省部署做好防汛抗旱工作

云南网讯(记者 王淑娟) 为做好今年水旱灾害防灾减灾工作,省水利厅近日组织召开全省水旱灾害防御形...

2023-03-14 08:13:54

乐于助人的词语(乐于助人的名言) 天天报道

1、病人之病,忧人之忧。2、出自:唐代白居易的《策林》。3、译文:为他人的病痛而难受,把他人的忧愁当...

2023-03-14 03:15:09

胡明轩22+5+6 徐杰21+9 杜锋被驱逐 广东轻取山西拿4连胜

胡明轩22+5+6徐杰21+9杜锋被驱逐广东轻取山西拿4连胜,杜锋,胡明轩,广东队,西蒙斯,山西省,广东省,总冠军,...

2023-03-13 22:12:23

saturday缩写_saturday_环球速讯

1、saturday是星期六的意思。2、Saturday英[ˈsætədeɪ]美[ˈsætərdeɪ]n 星期六双语例句:Shehadacallfromh

2023-03-13 19:10:22

乡村振兴补贴有哪些?补贴具体要怎么申请?

随着我国经济的快速发展,城乡差距逐渐扩大,农村经济面临诸多问题。为了推动乡村振兴,政府出台了一系...

2023-03-13 16:52:29

世界消息!大兴飞来百只银鸥,画面太美——

银鸥是广布于我国南北沿海和江河流域的一种旅鸟、冬候鸟,因头和颈部羽毛为白色,复羽和飞羽尖端亦具白...

2023-03-13 14:42:17

报道:康斯特:公司检测产品在工业领域各细分领域中应用广泛,除电力/石化等长流程工业外,制药、汽车/轨交/民航、仪表/传感器也均有应用

同花顺金融研究中心3月13日讯,有投资者向康斯特提问,您好,请问贵公司的压力及温度仪表,除了电力、石...

2023-03-13 11:58:03

【世界时快讯】无耻起来脸都不要了 日韩世界杯就差抄家伙 韩媒居然讽刺国青踢球脏

并且有韩国球员事后承认,他们是故意踢人的!2002年世界杯,韩国在1 8对阵意大利和1 4决赛对阵西班牙...

2023-03-13 09:45:16

贷款买车流程和注意事项

尿酸高不能吃什么食物清单(尿酸高不能吃什么食物一览表)

当前看点!(两会观察)透过两会释放信息,在华外企“看到机遇”

贞子这部电影吓死了多少人_贞子电影吓死多少人

电力行业考试中心网址_电力行业考试中心登录

速读:第八届侯登科纪实摄影奖作品展在深圳开幕

前沿热点:今日两个男一个女的言情小说_两男共享一女言情小说

头条焦点:养老账户余额是个人缴纳部分吗?账户余额怎么查询?

夜跑最佳时间跑多久_夜跑最佳时间

填空什么地念书_填空什么地跳舞-每日看点

丁威迪:要在防守端建立起我们的身份 没人想到大桥进攻如此棒

看热讯:东城:“薪火相传” 城管普法再次走进黑芝麻胡同小学

tcl智能电视-每日看点

如何用电脑共享wifi_用电脑共享wifi的方法

当前要闻:最喜小儿亡赖的下一句是什么_最喜小儿亡赖的原文及翻译

砂浆密度仪_砂浆密度

环球快报:海北州气象台发布寒潮黄色预警【2023-03-10】

光大集团完成发行20亿元超短期融资券 利率为2.21%

每日观点:wow盗贼换武器宏_盗贼换武器宏

建筑设计行业甲级资质或建筑工程设计专业甲级资质的承包范围-世界微动态

陕西黑猫短期债务承压 再融资或因环保问题频发受阻

【独家】“一站式工友之家”让工友“只跑一次”

陈泽迅还在吗_陈泽迅_全球热讯

阳光气质女生网名_阳光自信好听的女孩昵称精选

外交部发言人属什么级别_外交部发言人是什么级别_资讯推荐

x 广告
x 广告

Copyright @  2015-2022 亚洲家电网版权所有  备案号: 豫ICP备20022870号-9   联系邮箱:553 138 779@qq.com