context的字面意思是上下文,是一个比较抽象的词,字面上理解就是上下层的传递,上会把内容传递给下,在go中程序单位一般为goroutine,这里的上下文便是在goroutine之间进行传递。
根据现实例子来讲,最常看到context的便是web端。一个网络请求request请求服务端,每一个request都会开启一个goroutine,这个goroutine在逻辑处理中可能会去开启其他的goroutine,例如去开启一个MongoDB的连接,一个request的goroutine开启了很多个goroutine时候,需要对这些goroutine进行控制,这时候就需要context来进行对这些goroutine进行跟踪。即一个请求Request,会需要多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。
例子讲述完毕,用go的风格再讲一次。
在每一个goroutine在执行之前,都要知道程序当前的执行状态,这些状态都被封装在context变量中,要传递给要执行的goroutine中去,这个上下文就成为了传递与请求同生存周期变量的标准方法。
注意 context是在go 1.7版本之后引入的,以前版本的注意(go更新特别快,每一个版本都变得越来越好,自己第一次接触go语言的时候才1.9版本,实习公司用的好像是1.7,研发团队解体后现在实习用的版本是1.11 短时间版本就如此之大,1.10版本G-M模型改为G-P-M模型,听闻1.12社区会再次优化GC垃圾回收,引入分代)
Context接口
Context的接口定义的比较简洁,我们看下这个接口的方法。
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
这个接口共有4个方法,了解这些方法的意思非常重要,这样我们才可以更好的使用他们。
Deadline
方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Done
方法返回一个只读的chan,类型为struct{}
,我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done
方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。
Err
方法返回取消的错误原因,因为什么Context被取消。
Value
方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With
系列的函数了。
Context的继承衍生
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
这四个With
函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。
通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。
WithCancel
函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。 WithDeadline
函数,和WithCancel
差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
WithTimeout
和WithDeadline
基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
WithValue
函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value
方法访问到
引用飞雪无情的代码:
func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }
context.Background()
返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)
函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。在goroutine中,使用select调用
<-ctx.Done()
判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会继续进行监控。那么是如何发送结束指令的呢?这就是示例中的
cancel
函数啦,它是我们调用context.WithCancel(parent)
函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc
类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。
在引用一段多控制
func main() { ctx, cancel := context.WithCancel(context.Background()) go watch(ctx,"【监控1】") go watch(ctx,"【监控2】") go watch(ctx,"【监控3】") time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) } func watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Println(name,"监控退出,停止了...") return default: fmt.Println(name,"goroutine监控中...") time.Sleep(2 * time.Second) } } }
示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel
函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。
在引用一次潘少大佬的代码:
package mainimport ( "context" "crypto/md5" "fmt" "io/ioutil" "net/http" "sync" "time")type favContextKey string func main() { wg := &sync.WaitGroup{} values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"} ctx, cancel := context.WithCancel(context.Background()) for _, url := range values { wg.Add(1) subCtx := context.WithValue(ctx, favContextKey("url"), url) go reqURL(subCtx, wg) } go func() { time.Sleep(time.Second * 3) cancel() }() wg.Wait() fmt.Println("exit main goroutine") }func reqURL(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() url, _ := ctx.Value(favContextKey("url")).(string) for { select { case <-ctx.Done(): fmt.Printf("stop getting url:%s\n", url) return default: r, err := http.Get(url) if r.StatusCode == http.StatusOK && err == nil { body, _ := ioutil.ReadAll(r.Body) subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body))) wg.Add(1) go showResp(subCtx, wg) } r.Body.Close() //启动子goroutine是为了不阻塞当前goroutine,这里在实际场景中可以去执行其他逻辑,这里为了方便直接sleep一秒 // doSometing() time.Sleep(time.Second * 1) } } } func showResp(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("stop showing resp") return default: //子goroutine里一般会处理一些IO任务,如读写数据库或者rpc调用,这里为了方便直接把数据打印 fmt.Println("printing ", ctx.Value(favContextKey("resp"))) time.Sleep(time.Second * 1) } } }
首先调用context.Background()生成根节点,然后调用withCancel方法,传入根节点,得到新的子Context以及根节点的cancel方法(通知所有子节点结束运行),这里要注意:该方法也返回了一个Context,这是一个新的子节点,与初始传入的根节点不是同一个实例了,但是每一个子节点里会保存从最初的根节点到本节点的链路信息 ,才能实现链式。
程序的reqURL方法接收一个url,然后通过http请求该url获得response,然后在当前goroutine里再启动一个子groutine把response打印出来,然后从ReqURL开始Context树往下衍生叶子节点(每一个链式调用新产生的ctx),中间每个ctx都可以通过WithValue方式传值(实现通信),而每一个子goroutine都能通过Value方法从父goroutine取值,实现协程间的通信,每个子ctx可以调用Done方法检测是否有父节点调用cancel方法通知子节点退出运行,根节点的cancel调用会沿着链路通知到每一个子节点,因此实现了强并发控制,流程如图:
044svco84sif9rjebqagmar0fp.png
context使用规范
最后,Context虽然是神器,但开发者使用也要遵循基本法,以下是一些Context使用的规范:
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一个结构体当中,显式地传入函数。Context变量需要作为第一个参数使用,一般命名为ctx;
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO;
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的;
作者:不喜欢夜雨天
链接:https://www.jianshu.com/p/757bafad74ee