当前位置: 首页 > 科技观察

说说GoContext的正确使用姿势

时间:2023-03-20 23:06:15 科技观察

本文转载自微信公众号《我的大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。在Go语言中,Goroutine(协程),也就是关键字go,是一个众所周知的高级用法。这很聪明。说到Go,就会想到这门语言和关键字goroutine,最相关的就是context。背景通常在Go项目的开发中,几乎所有服务器(例如:HTTPServer)的默认实现在处理请求时都会启动一个新的goroutine进行处理。但是一开始有一个问题,当一个请求被取消或超时时,所有处理该请求的goroutines应该迅速退出,以便系统可以回收它们正在使用的任何资源。那时没有上下文标准库。非常令人沮丧。因此,Go在2014年正式公布了上下文标准库,形成了一个完整的闭环。但是有了上下文标准库,Go爱好者又开始疑惑了。前段时间有人问我:“使用Go上下文的正确姿势是什么”?(一张看不见的截图忘了在哪里问的)今天这条炸鱼带大家看完这篇文章。context的使用在Go的context使用中,我们经常会结合select关键字来监控是否结束、取消等。代码如下:constshortDuration=1*time.Millisecondfuncmain(){ctx,cancel:=context.WithTimeout(context.Background(),shortDuration)defercancel()select{case<-time.After(1*time.Second):fmt.Println("Mybrainisfriedfish")case<-ctx.Done():fmt.Println(ctx.Err())}}输出结果:contextdeadlineexceeded如果进一步结合goroutine,常见的例子是:func(ctxcontext.Context)<-chanint{dst:=make(chanint)n:=1gofunc(){for{select{case<-ctx.Done():returncasedst<-n:n++}}}()returndst}us项目中通常会有很多goroutines。这时会在goroutine中结合for+select处理上下文事件,达到跨goroutine控制的目的。正确的使用姿势是将上下文传递给第三方调用。在Go语言中,默认支持context已经是众所周知的规范。因此,当我们有第三方的调用请求时,需要传入上下文:funcmain(){req,err:=http.NewRequest("GET","https://eddycjy.com/",nil)iferr!=nil{fmt.Printf("http.NewRequesterr:%+v",err)return}ctx,cancel:=context.WithTimeout(req.Context(),50*time.Millisecond)defercancel()req=req.WithContext(ctx)resp,err:=http.DefaultClient.Do(req)iferr!=nil{fmt.Printf("http.DefaultClient.Doerr:%+v",err)return}deferresp.Body.Close()}像这样由于第三方开源库已经实现了基于上下文的超时控制,当你传入的时间到达时,调用就会中断。如果发现第三方开源库不支持context,建议运行修改。为了避免微服务系统下的级联故障,如果没有简单的手段去控制,会很麻烦。不要将上下文存储在结构类型中。你会发现在Go语言中,所有的第三方开源库和业务代码。所有将上下文作为方法的输入参数作为第一个形式参数。例如:标准要求每个方法的第一个参数都以上下文作为第一个参数,并使用ctx变量名惯用语。当然,我们不可能一招秒杀所有情况。确实很少有人将上下文放在结构中。基本常见于:底层基础库。DDD结构。每个请求都是独立的,每个请求的上下文自然不同。想清楚自己的应用使用场景很重要,否则遵循基本的Go规范就好了。在实际案例中,一些领导者会设计一个结构,仅仅是因为他们不想频繁传递上下文。结果一线RD天天要写NewXXX,有时甚至忘记了,还得背锅。函数调用链必须传播上下文。我们将把上下文作为第一个方法。本质目的是传播上下文并自行调用链上的各种控件:funcList(ctxcontext.Context,db*sqlx.DB)([]User,error){ctx,span:=trace.StartSpan(ctx"internal.user.List")deferspan.End()users:=[]User{}constq=`SELECT*FROMusers`iferr:=db.SelectContext(ctx,&users,q);err!=nil{returnnil,errors.Wrap(err,"selectingusers")}returnusers,nil}如上例,我们将传递的方法的上下文逐层传递给下一级方法。这里是将外部上下文传入List方法,再传入SQL执行方法的方法,解决了SQL执行语句的时间问题。context的继承与推导Go标准库context有如下标准的context推导方法:Context,CancelFunc)代码示例如下:WithTimeout(context.Background(),timeout)//chidrencontextnewCtx,cancel:=context.WithCancel(ctx)defercancel()//dosomething...}一般父上下文和子上下文是有区别的。我们需要确保程序中间上下文的行为对于多个goroutine同时使用是安全的。并且存在父子关系,父上下文关闭或超时,然后可以影响子上下文的程序。不要传nilcontext很多时候我们在创建context的时候,还不知道它的具体作用和下一步的用途。这时候可能直接使用context.Background方法:var(background=new(emptyCtx)todo=new(emptyCtx))funcBackground()Context{returnbackground}funcTODO()Context{returntodo}但是在实际context建议中,我们建议使用context.TODO方法来创建顶级上下文,并且在清楚实际上下文接下来将用于什么之前不要进行更改。Context只传递必要的值我们在使用context作为上下文的时候,往往会有信息传递的需求。比如在gRPC中,会有元数据的概念,而在gin中,context会被自己封装为参数管理。Go标准库上下文也提供了相关方法:typeContextfuncWithValue(parentContext,key,valinterface{})上下文代码示例如下:funcmain(){typefavContextKeystringf:=func(ctxcontext.Context,kfavContextKey){ifv:=ctx.Value(k);v!=nil{fmt.Println("foundvalue:",v)return}fmt.Println("keynotfound:",k)}k:=favContextKey("braininto")ctx:=context.WithValue(context.background(),k,"Friedfish")f(ctx,k)f(ctx,favContextKey("小咸鱼"))}输出结果:foundvalue:friedfishkeynotfound:Smallsaltedfishisinthespecification,我们建议context传递时,只携带必要的参数给其他方法,或者goroutines。即使在gRPC中,也会严格控制传出和传入的上下文参数。在业务场景中,上下文传递适用于传递必要的业务核心属性,如租户号、小程序ID等。可选参数不要放在上下文中,否则会弄得一团糟。总结对于第三方调用,需要传入context来控制远程调用。不要将上下文存储在结构类型中,尽可能将其作为函数的第一个参数传递。函数调用链必须传播上下文以实现对完整链的控制。上下文的继承和派生保证了父上下文和子上下文之间的联系。不要传递nil上下文,不确定的上下文应该使用TODO。context只传递必要的值,不要乱用可选参数。