Context详解
简介
官网
-
context go package
-
context-blog
Context是一个很特殊的接口,在go里面主要承担的责任是在边界(方法,线程等)传递上下文,这些上下文包括
- 取消信号
- 超时时间
- 特殊的参数
需要有几个注意点
- 不要传递nil的Context
- 不要在一个结构体中存储Context,而是要将它作为方法的参数传递过去,建议放在入参的第一个位置。
- 同一个Context可以传递给不同的go goroutines,Context是线程安全的。
- 不要将Context作为一个啥都能放的大而全的容器,以至于将函数的参数都放在里面。
出现背景
在开发中面临几个问题
- 规定一次操作的超时时间,如果操作超时,操作中止。
- 取消这次操作
- 此次操作中需要传递一些给下面操作的一些共有的参数,比如 用户标识。
我们以web开发为背景举个例子:
一次web请求包含redis操作,数据库操作,RPC操作。并且每一个请求都会有自己的go goroutine,需要在这个go goroutine中设置一些用户的信息,以便后续操作需要用到(比如 链路追踪,rpc调用中的用户信息打点参数等),当请求超时或者取消的时候,后续已经触发的操作能立即取消,并且释放相应的资源。(数据库取消查询,rpc取消调用)
接口
Context是一个接口。
// A Context carries a deadline, cancellation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// 返回一个channel,表示此Contenxt已经关闭
Done() <-chan struct{}
//表示channel取消或者关闭的原因
Err() error
// Context的超时时间,如果设置的话,ok返回为true,没有设置就是false
Deadline() (deadline time.Time, ok bool)
// 从Context通过key返回存储的Value,没有就是nil
Value(key interface{}) interface{}
}
方法分析
Context包中提供了下面的几个可导出的方法,这些方法已经实现了上面所说的功能,可以看到,他们必须要传递一个parent context(其实就是基本的Context),并且可以互相嵌套,从而生成一个树状结构的Context。例子如下:
这里需要注意下面几点
- context是要组成树状结构的。
- 子context在父类的基础上包装增加功能而来。
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "k1", "v1")
ctxCancel, cancelFunc := context.WithCancel(ctx)
cancelFunc()
timeoutCtx, c := context.WithTimeout(ctxCancel, time.Hour
}
下面介绍使用方式和基本原理
withValue
package main
import (
"context"
"fmt"
)
type User struct {
id int64
name string
}
func main() {
user := User{
id: 1,
name: "小明",
}
// 先构建一个基本的Context
ctx := context.Background()
// 用 WithValue 来包装ctx,将user存放在Context中,key为 user
ctx = context.WithValue(ctx, "user", user)
CheckNumberIsValid(ctx,"15909089432")
}
func CheckNumberIsValid(ctx context.Context,number string) (bool,error) {
user := ctx.Value("user").(User)
fmt.Printf("%v",user)
return true,nil
}
运行结果如下:
{1 小明}
WithValue方法如下
需要注意value
方法中的 for循环,要知道context是嵌套的,一个context只能存放一对值,要想继续存放必须context嵌套处理
,代码如下:
WithCancel
package main
import (
"golang.org/x/net/context"
"log"
"time"
)
func main() {
ctx := context.Background()
ctx, cancelFunc := context.WithCancel(ctx)
go func() {
log.Println("wait")
select {
case <-ctx.Done():
log.Println("done" + ctx.Err().Error())
}
}()
time.Sleep(1 * time.Second)
log.Println("ctx done ")
cancelFunc()
time.Sleep(time.Hour)
}
WithCancel
创建一个可取消的context,如上面所示,goroutine监听ctx done的channel,一秒之后调用取消函数,打印取消原因,调用结果如下:
源码分析如下
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// 创建cancelCtx,cancelCtx是一个不可导出的,并且实现了Context接口
c := newCancelCtx(parent)
// 传播取消操作
propagateCancel(parent, &c)
// 返回创建的cancelCtx,返回取消函数
return &c, func() { c.cancel(true, Canceled) }
}
// 入参为父Contex,和子Context
func propagateCancel(parent Context, child canceler) {
// 父ctx不能取消直接返回
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// 可以从父ctx中获取到信号,说明父context以及取消,此种情况下,子context也应该被取消掉
child.cancel(false, parent.Err())
return
default:
}
//判断ctx的的类型
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// 父ctx取消了,子ctx也应该取消掉
child.cancel(false, p.err)
} else {
// 将子ctx添加到父ctx
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 如果父类型不是cancelCtx,就需要启动goroutine
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
// 等待父ctx关闭,取消子ctx
child.cancel(false, parent.Err())
case <-child.Done():
// 子ctx关闭
}
}()
}
}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
// 父ctx是否关闭
if done == closedchan || done == nil {
return nil, false
}
// 看是否是cancelCtx
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
// 父ctx是cancelCtx,加载done channel,判断是否关闭
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
//cancel是canceler接口的方法,此接口表示 可直接取消。
// removeFromParent :是否从父context中移除
// err 错误原因
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
// 此ctx已经被取消掉
if c.err != nil {
c.mu.Unlock()
return
}
c.err = err
// 得到done channel,Done的channel是懒加载
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// 会依次关闭子ctx
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 从父ctx中移除此ctx
if removeFromParent {
removeChild(c.Context, c)
}
}
// canceler接口表示可以直接取消,
// 只有两个ctx实现了,
// 1: cancelCtx
// 2: timerCtx
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
// 实现Context接口,可提供多个子ctx,
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
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
// 懒加载,只有在调用Done方法的时候才会赖加载
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{}) // 关闭一个channel,还可以从channel中可读取
c.done.Store(d)
}
return d.(chan struct{})
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
从代码可以看出,cancelCtx提供了取消的能力,并且子ctx取消不会影响到父ctx,父ctx取消,子ctx会取消。
代码如下
package main
import (
"context"
"log"
"time"
)
func main() {
ctx := context.Background()
ctx1 := context.WithValue(ctx, "k1", "v1")
ctx11, cancelFunc11 := context.WithCancel(ctx)
ctx2, _ := context.WithCancel(ctx1)
ctx22 := context.WithValue(ctx11, "k2", "v2")
go func() {
log.Println("ctx22 wait")
select {
case <-ctx22.Done():
log.Println("ctx22 done,",ctx22.Err())
}
}()
go func() {
log.Println("ctx2 wait")
select {
case <-ctx2.Done():
log.Println("ctx2 done,",ctx2.Err())
}
}()
time.Sleep(time.Second)
log.Println("call cancelFunc11")
cancelFunc11()
time.Sleep(time.Hour)
}
运行结果如下:
从代码可以反推有几种情况验证一下:
- 父ctx被取消,然后在用父ctx创建子ctx,子ctx会怎么样?
- 父ctx不是cancelCtx,子ctx会怎么样?
代码如下
-
package main import ( "context" "log" "time" ) func main() { ctx := context.Background() ctx, cancelFunc := context.WithCancel(ctx) // 父ctx取消 cancelFunc() ctx1, cancelFunc1 := context.WithCancel(ctx) go func() { log.Println("ctx1 wait") select { case <-ctx1.Done(): log.Println("ctx1 done,",ctx1.Err()) } }() time.Sleep(time.Second) log.Println("call cancelFunc1") // 调用子ctx cancelFunc1() time.Sleep(time.Hour) }
原则是父影响子,子不影响父
-
package main import ( "context" "errors" "log" "sync/atomic" "time" ) type MyContext struct { done atomic.Value // of chan struct{}, created lazily, closed by first cancel call err error } func (m *MyContext) Deadline() (deadline time.Time, ok bool) { return time.Time{}, false } func (m *MyContext) Done() <-chan struct{} { doneChannel := make(chan struct{}) m.done.Store(doneChannel) return doneChannel } func (m *MyContext) Err() error { return m.err } func (m *MyContext) Value(key any) any { return nil } func main() { // 创建自定义context ctx := &MyContext{} // 创建cancel ctx ctx1, _ := context.WithCancel(ctx) // 等待ctx2取消 go func() { log.Println("ctx2 wait") select { case <-ctx1.Done(): log.Println("ctx2 done,",ctx1.Err()) } }() // 过一秒 父ctx 关闭 time.Sleep(time.Second) // 这是我自己写的演示,不太规范,但能说明问题 log.Println("parent context done") ctx.err = errors.New("my context done close") c := ctx.done.Load().(chan struct{}) close(c) time.Sleep(time.Hour) }
结果如下:
结果很显然,和上面一样,父context被取消,子context也得被取消
。回过头在来看看源码中逻辑:
Context的基本原则
到这里,体会一下context的原则
- 父context 完成,子context也需要完成(done)
- 子context完成,父context不会受到影响
- 需要有取消的操作,有两种方式,手动和自动,手动的前提是此context实现了cancel接口,自动的话,需要启动一个goroutine,监听父Context的Done信号,从而取消。
带着上面的原则,继续往下看剩余的方法。
WithDeadline
context提供了超时时间的能力。代码如下
package main
import (
"context"
"log"
"time"
)
func main() {
ctx := context.Background()
// 截至时间是两秒之后
ctx, cancelFunc := context.WithDeadline(ctx, time.Now().Add(time.Second*2))
go func() {
log.Println("ctx wait")
select {
case <-ctx.Done():
log.Println("ctx done,",ctx.Err())
}
}()
// 五秒之后调用取消函数
time.Sleep(time.Second * 5)
log.Println("ctx done")
cancelFunc()
time.Sleep(time.Hour)
}
结果如下:
源码分析如下:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// parent有超时时间,并且比子ctx的超时时间小,也就是父context的deadline比子deadline小。
// 直接返回了cancelCtx,本质来说,还是以父为主
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 从这往下走的前提是父context有deadline并且deadline比子的deadline大
// 或者父context没有deadline能力
// 创建timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 传播cancel,
propagateCancel(parent, c)
// 算当前时间的差值
dur := time.Until(d)
if dur <= 0 {
// 比当前时间小
// 取消自己,并且从父context中移除自己
c.cancel(true, DeadlineExceeded)
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
//创建一个timer,定时取消
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
// timerCtx实现了cancel接口,并且聚合了cancelCtx,
// 在cancelCtx的基础上增加了定时取消的能力(timer)
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 调用cancel取消
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
从源码可以看到,WithDeadline
的底层实现是timerCtx
timerCtx
聚合了cancelCtx
,有取消的能力,并且通过timer
增加了超时自动取消的能力,它把上面说的手动取消和自动取消结合在一块了。
timerCtx
有超时时间的功能,为了上面所说的原则,需要在创建的时候通过超时时间来判断父context和子context是否已经完成。
从代码可以推测父子deadline的超时有几种情况需要验证:
- 父 > 子
- 子 < 父
- 子 < 当前时间
验证代码如下:
-
父是父,子是子,分开的。
还有一点需要补充:
WithTimeout
提供了超时时间的能力。调用的是WithDeadline
,这里就不在解释了
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
Context到这里就结束了。