引言
在之前的博客中,讲到过 Go 语言的优势之一是支持并发,而且也谈到过,使用 WaitGroup 控制多个 goroutine 实现并发。
本篇博客将谈一下 Go 语言的另一种并发模型—— Context。
介绍
对于 Go 语言开发者来说 context(上下文)包一定不会陌生。
应用场景:
- 有一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些 goroutine又可能会开启其他的goroutine。这样的话,我们就可以通过Context,来跟踪这些goroutine,并且通过Context来控制他们的目的,这就是Go语言为我们提供的Context,中文可以称之为“上下文”。
- 在 Go http 包的 Server 中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和 RPC 服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的 token、请求的截止时间。当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
注意:go1.6及之前版本请使用golang.org/x/net/context。go1.7及之后已移到标准库context。
遵循规则
遵循以下规则,以保持包之间的接口一致,并启用静态分析工具以检查上下文传播。
- 不要将 Contexts 放入结构体,相反
context
应该作为第一个参数传入,命名为ctx
。
func DoSomething(ctx context.Context,arg Arg)error {
// ... use ctx ...
}
- 即使函数允许,也不要传入
nil
的 Context。如果不知道用哪种 Context,可以使用context.TODO()
。 - 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数
- 相同的 Context 可以传递给在不同的
goroutine
;Context 是并发安全的。
context 包
包的核心是 struct Context,声明如下:
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
- Done 方法在Context被取消或超时时返回一个close的channel,close的channel可以作为广播通知,告诉给context相关的函数要停止当前工作然后返回。
当一个父operation启动一个goroutine用于子operation,这些子operation不能够取消父operation。下面描述的WithCancel函数提供一种方式可以取消新创建的Context.
Context可以安全的被多个goroutine使用。开发者可以把一个Context传递给任意多个goroutine然后cancel这个context的时候就能够通知到所有的goroutine。
- Err方法返回context为什么被取消。
- Deadline返回context何时会超时。
- Value返回context相关的数据。
所有方法
func Background() Context
func TODO() 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
Context 的调用应该是链式的,通过 WithCancel,WithDeadline,WithTimeout 或 WithValue派生出新的 Context。当父 Context 被取消时,其派生的所有 Context 都将取消。
通过 context.WithXXX 都将返回新的 Context 和 CancelFunc。调用 CancelFunc 将取消子代,移除父代对子代的引用,并且停止所有定时器。未能调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。go vet 工具检查所有流程控制路径上使用 CancelFuncs。
Context 的实现方法
WithCancel 例子
WithCancel 以一个新的 Done channel 返回一个父 Context 的拷贝。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
下面的例子演示使用一个可取消的上下文,以防止 goroutine 泄漏。示例函数结束时,defer 调用 cancel 方法,gen goroutine 将返回而不泄漏。
func main() {
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
WithDeadline 例子
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
......
当派生出的子 Context 的 deadline 在父 Context之 后,直接返回了一个父 Context 的拷贝。
WithDeadline 的最后期限调整为不晚于 d 返回父上下文的副本。如果父类的截止日期已经早于 d,WithDeadline (父,d) 是在语义上等效为父。返回的上下文完成的通道关闭的最后期限期满后,返回的取消函数调用时,或当父上下文完成的通道关闭,以先发生者为准。
官方实例如下:
package main
import (
"context"
"fmt"
"time"
)
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
WithTimeout 例子
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
看看官方例子:
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
}
WithValue 例子
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
WithValue 返回的父与键关联的值在 val 的副本。
使用上下文值仅为过渡进程和 Api 的请求范围的数据,而不是将可选参数传递给函数。
提供的键必须是可比性和应该不是字符串类型或任何其他内置的类型以避免包使用的上下文之间的碰撞。WithValue 用户应该定义自己的键的类型。为了避免分配分配给接口 {} 时,上下文键经常有具体类型结构 {}。另外,导出的上下文关键变量静态类型应该是一个指针或接口。
官方例子:
package main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
再看一个通过 Context 传递一些必须的元数据,这些数据会附加在 Context 上以供使用:
var key string="name"
func main() {
ctx, cancel := context.WithCancel(context.Background())
//附加值
valueCtx:=context.WithValue(ctx,key,"【监控1】")
go watch(valueCtx)
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context) {
for {
select {
case <-ctx.Done():
//取出值
fmt.Println(ctx.Value(key),"监控退出,停止了...")
return
default:
//取出值
fmt.Println(ctx.Value(key),"goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}
通过传递参数的方式,把 name
的值传递给监控函数。在这个例子里,我们实现一样的效果,但是通过的是 Context 的 Value 的方式。在这里可以使用 context.WithValue
方法附加一对 K-V 的键值对,这里 Key 必须是等价性的,也就是具有可比性;Value 值要是线程安全的。这样就生成了一个新的 Context,这个新的 Context 带有这个键值对,在使用的时候,可以通过 Value 方法读取 ctx.Value(key)
。
记住,使用 WithValue 传值,一般是必须的值,不要什么值都传递。
参考
https://juejin.im/post/5a6873fef265da3e317e55b6
https://studygolang.com/articles/13598
https://www.flysnow.org/2017/05/12/go-in-action-go-context.html