Golang 如何正确使用 Context
视频信息
How to correctly use package context
by Jack Lindamood
at Golang UK Conf. 2017
视频:
https://www.youtube.com/watch?v=-_B5uQ4UGi0
博文:
https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39
为什么需要 Context
每一个长请求都应该有个超时限制
需要在调用中传递这个超时
比如开始处理请求的时候我们说是 3 秒钟超时
那么在函数调用中间,这个超时还剩多少时间了?
需要在什么地方存储这个信息,这样请求处理中间可以停止
如果进一步考虑。
如上图这样的 RPC 调用,开始调用 RPC 1 后,里面分别调用了 RPC 2, RPC 3, RPC 4,等所有 RPC 用成功后,返回结果。
这是正常的方式,但是如果 RPC 2 调用失败了会发生什么?
RPC 2 失败后,如果没有 Context 的存在,那么我们可能依旧会等所有的 RPC 执行完毕,但是由于 RPC 2 败了,所以其实其它的 RPC 结果意义不大了,我们依旧需要给用户返回错误。因此我们白白的浪费了 10ms,完全没必要去等待其它 RPC 执行完毕。
那如果我们在 RPC 2 失败后,就直接给用户返回失败呢?
用户是在 30ms 的位置收到了错误消息,可是 RPC 3 和 RPC 4 依然在没意义的运行,还在浪费计算和IO资源。
所以理想状态应该是如上图,当 RPC 2 出错后,除了返回用户错误信息外,我们也应该有某种方式可以通知 RPC 3 和 RPC 4,让他们也停止运行,不再浪费资源。
所以解决方案就是:
用信号的方式来通知请求该停了
包含一些关于什么时间请求可能会结束的提示(超时)
用 channel 来通知请求结束了
那干脆让我们把变量也扔那吧。?
在 Go 中没有线程/go routine 变量
其实挺合理的,因为这样就会让 goroutine 互相产生依赖
非常容易被滥用
Context 实现细节
context.Context:
是不可变的(immutable)树节点
Cancel 一个节点,会连带 Cancel 其所有子节点 (从上到下)
Context values 是一个节点
Value 查找是回溯树的方式 (从下到上)
示例 Context 链
完整代码:https://play.golang.org/p/ddpofBV1QS
123456789 |
package mainfunc tree() { ctx1 := context.Background() ctx2, _ := context.WithCancel(ctx1) ctx3, _ := context.WithTimeout(ctx2, time.Second * 5) ctx4, _ := context.WithTimeout(ctx3, time.Second * 3) ctx5, _ := context.WithTimeout(ctx3, time.Second * 6) ctx6 := context.WithValue(ctx5, "userID", 12)} |
如果这样构成的 Context 链,其形如下图:
那么当 3 秒超时到了时候:
可以看到 ctx4 超时退出了。
当 5秒钟 超时到达时:
可以看到,不仅仅 ctx3 退出了,其所有子节点,比如 ctx5 和 ctx6 也都退出了。
context.Context API
基本上是两类操作:
3个函数用于限定什么时候你的子节点退出;
1个函数用于设置请求范畴的变量
12345678
type Context interface { // 啥时候退出 Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error // 设置变量 Value(key interface{}) interface{}}
什么时候应该使用 Context?
每一个 RPC 调用都应该有超时退出的能力,这是比较合理的 API 设计
不仅仅 是超时,你还需要有能力去结束那些不再需要操作的行为
context.Context 是 Go 标准的解决方案
任何函数可能被阻塞,或者需要很长时间来完成的,都应该有个 context.Context
如何创建 Context?
在 RPC 开始的时候,使用 context.Background()
有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。
如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO()
如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子)
如何集成到 API 里?
如果有 Context,将其作为第一个变量。
如 func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
有些人把 context 放到中间的某个变量里去,这很不合习惯,不要那么做,放到第一个去。
将其作为可选的方式,用 request 结构体方式。
如:func (r *Request) WithContext(ctx context.Context) *Request
Context 的变量名请用 ctx(不要起一些诡异的名字?)
Context 放哪?
把 Context 想象为一条河流流过你的程序(另一个意思就是说不要喝河里的水……?)
理想情况下,Context 存在于调用栈(Call Stack) 中
不要把 Context 存储到一个 struct 里
除非你使用的是像 http.Request 中的 request 结构体的方式
request 结构体应该以 Request 结束为生命终止
当 RPC 请求处理结束后,应该去掉对 Context 变量的引用(Unreference)
Request 结束,Context 就应该结束。(这俩是一对儿,不求同年同月同日生,但求同年同月同日死……?)
Context 包的注意事项
要养成关闭 Context 的习惯
特别是 超时的 Contexts
如果一个 context 被 GC 而不是 cancel 了,那一般是你做错了
12
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)defer cancel()
使用 Timeout 会导致内部使用 time.AfterFunc,从而会导致 context 在计时器到时之前都不会被垃圾回收。
在建立之后,立即 defer cancel() 是一个好习惯。
终止请求 (Request Cancellation)
当你不再关心接下来获取的结果的时候,有可能会 Cancel 一个 Context?
以 golang.org/x/sync/errgroup 为例,errgroup 使用 Context 来提供 RPC 的终止行为。
123456 |
type Group struct { cancel func() wg sync.WaitGroup errOnce sync.Once err error} |
创建一个 group 和 context:
1234 |
func WithContext(ctx context.Context) (*Group, context.Context) { ctx, cancel := context.WithCancel(ctx) return &Group{cancel: cancel}, ctx} |
这样就返回了一个可以被提前 cancel 的 group。
而调用的时候,并不是直接调用 go func(),而是调用 Go(),将函数作为参数传进去,用高阶函数的形式来调用,其内部才是 go func() 开启 goroutine。
1234567891011121314 |
func (g *Group) Go(f func() error) { g.wg.Add(1) go func() { defer g.wg.Done() if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { g.cancel() } }) } }()} |
当给入函数 f 返回错误,则使用 sync.Once 来 cancel context,而错误被保存于 g.err 之中,在随后的 Wait() 函数中返回。
1234567 |
func (g *Group) Wait() error { g.wg.Wait() if g.cancel != nil { g.cancel() } return g.err} |
注意:这里在 Wait() 结束后,调用了一次 cancel()。
123456789101112131415161718192021 |
package mainfunc DoTwoRequestsAtOnce(ctx context.Context) error { eg, egCtx := errgroup.WithContext(ctx) var resp1, resp2 *http.Response f := func(loc string, respIn **http.Response) func() error { return func() error { reqCtx, cancel := context.WithTimeout(egCtx, time.Second) defer cancel() req, _ := http.NewRequest("GET", loc, nil) var err error *respIn, err = http.DefaultClient.Do(req.WithContext(reqCtx)) if err == nil && (*respIn).StatusCode >= 500 { return errors.New("unexpected!") } return err } } eg.Go(f("http://localhost:8080/fast_request", &resp1)) eg.Go(f("http://localhost:8080/slow_request", &resp2)) return eg.Wait()} |
在这个例子中,同时发起了两个 RPC 调用,当任何一个调用超时或者出错后,会终止另一个 RPC 调用。这里就是利用前面讲到的 errgroup 来实现的,应对有很多并非请求,并需要集中处理超时、出错终止其它并发任务的时候,这个 pattern 使用起来很方便。
Context.Value - Request 范畴的值
context.Value API 的万金油(duct tape)
胶带(duct tape) 几乎可以修任何东西,从破箱子,到人的伤口,到汽车引擎,甚至到NASA登月任务中的阿波罗13号飞船(Yeah! True Story)。所以在西方文化里,胶带是个“万能”的东西。在中文里,恐怕万金油是更合适的对应词汇,从头疼、脑热,感冒发烧,到跌打损伤几乎无所不治。
当然,治标不治本,这点东西方文化中的潜台词都是一样的。这里提及的 context.Value 对于 API 而言,就是这类性质的东西,啥都可以干,但是治标不治本。
value 节点是 Context 链中的一个节点
123456789101112131415
package contexttype valueCtx struct { Context key, val interface{}}func WithValue(parent Context, key, val interface{}) Context { // ... return &valueCtx{parent, key, val}}func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
可以看到,WithValue() 实际上就是在 Context 树形结构中,增加一个节点罢了。
Context 是 immutable 的。
约束 key 的空间
为了防止树形结构中出现重复的键,建议约束键的空间。比如使用私有类型,然后用 GetXxx() 和 WithXxxx() 来操作私有实体。
1234567891011 |
type privateCtxType stringvar ( reqID = privateCtxType("req-id"))func GetRequestID(ctx context.Context) (int, bool) { id, exists := ctx.Value(reqID).(int) return id, exists}func WithRequestID(ctx context.Context, reqid int) context.Context { return context.WithValue(ctx, reqID, reqid)} |
这里使用 WithXxx 而不是 SetXxx 也是因为 Context 实际上是 immutable 的,所以不是修改 Context 里某个值,而是产生新的 Context 带某个值。
Context.Value 是 immutable 的
再多次的强调 Context.Value 是 immutable 的也不过分。
context.Context 从设计上就是按照 immutable (不可变的)模式设计的
同样,Context.Value 也是 immutable 的
不要试图在 Context.Value 里存某个可变更的值,然后改变,期望别的 Context 可以看到这个改变
更别指望着在 Context.Value 里存可变的值,最后多个 goroutine 并发访问没竞争冒险啥的,因为自始至终,就是按照不可变来设计的
比如设置了超时,就别以为可以改变这个设置的超时值
在使用 Context.Value 的时候,一定要记住这一点
应该把什么放到 Context.Value 里?
应该保存 Request 范畴的值
任何关于 Context 自身的都是 Request 范畴的(这俩同生共死)
从 Request 数据衍生出来,并且随着 Request 的结束而终结
什么东西不属于 Request 范畴?
在 Request 以外建立的,并且不随着 Request 改变而变化
比如你 func main() 里建立的东西显然不属于 Request 范畴
数据库连接
如果 User ID 在连接里呢?(稍后会提及)
全局 logger
如果 logger 里需要有 User ID 呢?(稍后会提及)
那么用 Context.Value 有什么问题?
不幸的是,好像所有东西都是由请求衍生出来的
那么我们为什么还需要函数参数?然后干脆只来一个 Context 就完了?
123
func Add(ctx context.Context) int { return ctx.Value("first").(int) + ctx.Value("second").(int)}
曾经看到过一个 API,就是这种形式:
1234 |
func IsAdminUser(ctx context.Context) bool { userID := GetUser(ctx) return authSingleton.IsAdmin(userID)} |
这里API实现内部从 context 中取得 UserID,然后再进行权限判断。但是从函数签名看,则完全无法理解这个函数具体需要什么、以及做什么。
代码要以可读性为优先设计考虑。
别人拿到一个代码,一般不是掉进函数实现细节里去一行行的读代码,而是会先浏览一下函数接口。所以清晰的函数接口设计,会更加利于别人(或者是几个月后的你自己)理解这段代码。
一个良好的 API 设计,应该从函数签名就清晰的理解函数的逻辑。如果我们将上面的接口改为:
1 |
func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool |
我们从这个函数签名就可以清楚的知道:
这个函数很可能可以提前被 cancel
这个函数需要 User ID
这个函数需要一个authenticator来
而且由于 authenticator 是传入参数,而不是依赖于隐式的某个东西,我们知道,测试的时候就很容易传入一个模拟认证函数来做测试
userID 是传入值,因此我们可以修改它,不用担心影响别的东西
所有这些信息,都是从函数签名得到的,而无需打开函数实现一行行去看。
那什么可以放到 Context.Value 里去?
现在知道 Context.Value 会让接口定义更加模糊,似乎不应该使用。那么又回到了原来的问题,到底什么可以放到 Context.Value 里去?换个角度去想,什么不是衍生于 Request?
Context.Value 应该是告知性质的东西,而不是控制性质的东西
应该永远都不需要写进文档作为必须存在的输入数据
如果你发现你的函数在某些 Context.Value 下无法正确工作,那就说明这个 Context.Value 里的信息不应该放在里面,而应该放在接口上。因为已经让接口太模糊了。
什么东西不是控制性质的东西?
Request ID
而 logger 本身不是 Request 范畴,所以 logger 不应该在 Context 里
非 Request 范畴的 logger 应该只是利用 Context 信息来修饰日志
只是给每个 RPC 调用一个 ID,而没有实际意义
这就是个数字/字符串,反正你也不会用其作为逻辑判断
一般也就是日志的时候需要记录一下
User ID (如果仅仅是作为日志用)
Incoming Request ID
什么显然是控制性质的东西?
数据库连接
显然会非常严重的影响逻辑
因此这应该在函数参数里,明确表示出来
认证服务(Authentication)
显然不同的认证服务导致的逻辑不同
也应该放到函数参数里,明确表示出来
例子
调试性质的 Context.Value - net/http/httptrace
https://medium.com/@cep21/go-1-7-httptrace-and-context-debug-patterns-608ae887224a
12345678910111213141516 |
package mainfunc trace(req *http.Request, c *http.Client) { trace := &httptrace.ClientTrace{ GotConn: func(connInfo httptrace.GotConnInfo) { fmt.Println("Got Conn") }, ConnectStart: func(network, addr string) { fmt.Println("Dial Start") }, ConnectDone: func(network, addr string, err error) { fmt.Println("Dial done") }, } req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) c.Do(req)} |
net/http 是怎么使用 httptrace 的?
如果有 trace 存在的话,就执行 trace 回调函数
这只是告知性质,而不是控制性质
http 不会因为存在 trace 与否就有不同的执行逻辑
这里只是告知 API 的用户,帮助用户记录日志或者调试
因此这里的 trace 是存在于 Context 里的
123456789
package httpfunc (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) { // ... trace := httptrace.ContextClientTrace(req.Context()) // ... if trace != nil && trace.WroteHeaders != nil { trace.WroteHeaders() }}
回避依赖注入 - github.com/golang/oauth2
这里比较诡异,使用 ctx.Value 来定位依赖
不推荐这样做
这里这样做基本上只是为了满足测试需求
12345678
package mainimport "github.com/golang/oauth2"func oauth() { c := &http.Client{Transport: &mockTransport{}} ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c) conf := &oauth2.Config{ /* ... */ } conf.Exchange(ctx, "code")}
人们滥用 Context.Value 的原因
中间件的抽象
很深的函数调用栈
混乱的设计
context.Value 并没有让你的 API 更简洁,那是假象,相反,它让你的 API 定义更加模糊。
总结 Context.Value
对于调试非常方便
将必须的信息放入 Context.Value 中,会让接口定义更加不透明
如果可以尽量明确定义在接口
尽量不要用 Context.Value
总结 Context
所有的长的、阻塞的操作都需要 Context
errgroup 是构架于 Context 之上很好的抽象
当 Request 的结束的时候,Cancel Context
Context.Value 应该被用于告知性质的事物,而不是控制性质的事物
约束 Context.Value 的键空间
Context 以及 Context.Value 应该是不可变的(immutable),并且应该是线程安全
Context 应该随 Request 消亡而消亡
Q&A
数据库的访问也用 Context 么?
之前说过长时间、可阻塞的操作都用 Context,数据库操作也是如此。不过对于超时 Cancel 操作来说,一般不会对写操作进行 cancel;但是对于读操作,一般会有 Cancel 操作。
原文
https://blog.lab99.org/post/golang-2017-10-27-video-how-to-correctly-use-package-context.html
< END >
喜欢就点个在看 or 转发个朋友圈呗
衣舞晨风
Golang 如何正确使用 Context相关推荐
- go get报错unrecognized import path “golang.org/x/net/context”…
今天安装gin框架,首先下载gin,命令如下: go get github.com/mattn/go-sqlite3 结果报错: package golang.org/x/net/context: u ...
- go get报错:unrecognized import path “golang.org/x/net/context”…
今天安装gin框架,首先下载gin,命令如下: go get github.com/mattn/go-sqlite3 结果报错: package golang.org/x/net/context: u ...
- Golang 并发编程之Context
Context 是 Golang 中非常有趣的设计,它与 Go 语言中的并发编程有着比较密切的关系,在其他语言中我们很难见到类似 Context 的东西,它不仅能够用来设置截止日期.同步『信号』还能用 ...
- Golang中WaitGroup、Context、goroutine定时器及超时学习笔记
原文连接:http://targetliu.com/2017/5/2... 好久没有发过文章了 - -||,今天发一篇 golang 中 goroutine 相关的学习笔记吧,以示例为主. WaitG ...
- Golang如何正确的停止Ticker
Golang可以利用time包的Ticker实现定时器的作用,最近使用Ticker时,发现调用Ticker的Stop方法无法正确的停止Ticker,协程会阻塞在等待Ticker的C通道处,精简后的代码 ...
- golang优雅的使用context
文章目录 优雅的关闭goroutine sync.WaitGroup 实现 channel + select 实现 context 实现 优雅的关闭多个goroutine嵌套 net/http包中的c ...
- golang不能正确显示emoji的处理
golang在使用gorm的时候,emoji会变成????,这一看应该就是字符的问题了,数据库改字段已经修改为utf8mb4了,显示出来的还是????. gorm使用的是github.com/jinz ...
- Golang Devops项目开发(1)
1.1 GO语言基础 1 初识Go语言 1.1.1 开发环境搭建 参考文档:<Windows Go语言环境搭建> 1.2.1 Go语言特性-垃圾回收 a. 内存自动回收,再也不需要开发人员 ...
- golang 上下文 Context
上下文 context.Context Go 语言中用来设置截止日期.同步信号,传递请求相关值的结构体.上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们 ...
最新文章
- MIT请来了一群经济学家,就AI是否会带来大规模失业展开了一场辩论
- 集群理论讲解(续三)
- 基于 Laravel 5 构建的、支持模块化和多语言的 CMS —— AsgardCMS
- js的时间函数实现一个电子表
- HBase读链路分析
- Ubuntu删除和新建用户
- 找不到可安装的isam怎么解决_安装系统找不到硬盘怎么办
- 团队开发——用户需求报告
- codeforces 123D. String(后缀数组+单调栈,好题)
- python爬虫电影资源_python爬虫批量获取最新电影资源
- tomcat 如何查看tomcat版本及位数
- 函数的极值点、零点、驻点、拐点的理解
- 登录服务器时显示 IE COOKIE阻止,[IE问题]IE相关设置-智明协同
- 互联网最新创新创业项目
- Tomcat 优化
- 以人为本的四大用户体验原则
- BrandHouse在蓝筹中国基金领投的首轮融资中筹得400万欧元
- 电磁中间继电器DZJ-206/220VAC
- java打印代码执行耗时
- 上海市街道划分矢量数据