Context是什么?
context其实是一个接口,提供了四种方法,在官方go语言中对context接口提供了四种基本类型的实现
再简单一点,对于Javaer来说,Go的Context就是Java的ThreadLocal
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline:设置context.Context被取消的时间,即截止时间
- Done:返回一个 只读的Channel,当Context被取消或者到达截止时间,这个Channel就会被关闭,表示Context的链路结束
- Err:返回context.Context结束的原因,会在Done返回的Channel被关闭时才会返回非空的值
- 如果是context.Context被取消,返回Canceled
- 如果是context.Context超时,返回DeadlineExceeded
- Value:从context.Context中获取键对应的值,类似于map的get方法,对于同一个context,多次调用Value并传入相同的Key,会返回相同的结果,如果没有对应的key,则返回nil,键值对是通过WithValue方法写入的
如何创建Context及Context的派生函数With系列
根context创建,源码上看没有太多的区别,在一般情况下,当前函数没有上下文作为入惨,我们都会用Background()创建
func Background() Context {
return background
}
func TODO() Context {
return todo
}
func main() {
ctx := context.TODO()
ctx1 := context.Background()
}
context派生函数,为了让context在我们的程序中发挥作用,我们要依靠context包提供的With系列函数来进行派生
主要是以下几个派生函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val any) Context
Context使用场景和用途
在明确context接口提供了哪些方法后,看似联想出这些实现是为了解决什么问题
主要是两点作用:上下文信息传递和写成的取消控制
比如Context可以用来在goroutine之间传递上下文信息,比如传递请求的trace_id,以便追踪全局唯一请求
还可以用来做取消控制,通过取消信号和超时时间来控制子goroutine的退出,防止goroutine泄漏
派生函数使用举例:
- WithValue(),一般项目中用这个方法用于上下文信息的传递,比如请求唯一id,trace_id等
func func1(ctx context.Context) {
fmt.Printf("name is: %s", ctx.Value("name").(string))
}
func main() {
ctx := context.WithValue(context.Background(), "name", "zhangsan")
go func1(ctx)
time.Sleep(time.Second)
}
输出:
name is: zhangsan
-
WithCancel() 取消控制函数,需要一个父context作为参数,从context.Context中衍生出一个新的子context和取消函数CancelFunc()
通过将这子context传递到新的goroutine中来控制这些goroutine的关闭,一旦我们执行返回的取消函数CancelFunc
当前的上下文以及它的子上下文都会被取消,所有的Goroutine都会同步的收到取消信号
func main() {
ctx, cancel := context.WithCancel(context.Background())
go Watch(ctx, "goroutine1")
go Watch(ctx, "goroutine2")
time.Sleep(6 * time.Second) // 让goroutine1和goroutine2执行6s
fmt.Println("end watching!!!")
cancel() // 通知goroutine1和goroutine2关闭
time.Sleep(1 * time.Second)
}
func Watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s exit!\n", name) // 主goroutine调用cancel后,会发送一个信号到ctx.Don
return
default:
fmt.Printf("%s watching...\n", name)
time.Sleep(time.Second)
}
}
}
输出:
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
end watching!!!
goroutine1 exit!
goroutine2 exit!
-
WithDeadline() 和 WithTimeout() 一起看,其实作用是差不多的,在传参数上有所区别
WithDeadline(parent Context, d time.Time) (Context, CancelFunc) 这个就是截止时间
WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 这个是时间长度
具体的区别细节,大家看下面这段代码即可
func main() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second))
defer cancel()
go Watch(ctx, "goroutine1")
go Watch(ctx, "goroutine2")
time.Sleep(6 * time.Second) // 让goroutine1和goroutine2执行6s
fmt.Println("end watching!!!")
}
func Watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s exit!\n", name) // 4s之后收到信号
return
default:
fmt.Printf("%s watching...\n", name)
time.Sleep(time.Second)
}
}
}
/** ---------------------------------------------------------------------------------------- **/
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
go Watch(ctx, "goroutine1")
go Watch(ctx, "goroutine2")
time.Sleep(6 * time.Second) // 让goroutine1和goroutine2执行6s
fmt.Println("end watching!!!")
}
func Watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s exit!\n", name) // 主goroutine调用cancel后,会发送一个信号到ctx.Done
return
default:
fmt.Printf("%s watching...\n", name)
time.Sleep(time.Second)
}
}
}
Context思考
- 为什么 WithCancel 可以控制所有子Goroutine停止?从源码角度分析一下
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) }
}
这里我们可以看到 c := newCancelCtx(parent) 新指向了CancelCtx,我们来看这个结构体
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
children map[canceler]struct{} 所有子协程都会放入这个children的map里面,取消cancel的时候,递归的取消所有子协程
具体源码在GoLand上看吧,这里只取出一部分逻辑
for child := range c.children {
// 递归取消所有子节点
child.cancel(false, err)
}
那么Goroutine如何感知呢?换句话说父协程怎么操控的子协程,cancel的内部细节是什么?
Goroutine 通过 select监听ctx.Done() 来感知取消cancel的操作
func Watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s exit!\n", name)
return
default:
// 业务逻辑...
}
}
}
当 ctx.Done() 返回的 channel 被关闭(即 cancel 被调用),case <-ctx.Done() 会被触发,Goroutine 执行相应的退出逻辑。
Context注意点
- 注意WithValue()返回值是一个新的context。
context.WithValue均不会修改父上下文,而是返回全新的子上下文
func WithValue(parent Context, key, val any) 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}
}
来看注意点 ,初学的时候,Java那一套没有完全转变过来,以为context放进去WithValue()就可以了,其实不是
ctx := context.TODO()
context.WithValue(ctx,"key","value")
正确的写法:
ctx1 := context.TODO()
ctx1 = context.WithValue(ctx,"key","value")
ctx里面是没有这个key的,ctx1才有这个key,从源码中我们也是能看得出的,&valueCtx{parent, key, val} 返回了新的context