Golang 中正确理解context

context(上下文)作用

golang中的context主要为了处理单个请求域与多个goroutine之间的数据共享,取消信号,截止时间等问题,在一个网络请求中,创建多个goroutine,就可以通过context去追踪所有的goroutine,

 

context结构

type Context interface {
  # conetext是否会被取消以取消的时间
    Deadline() (deadline time.Time, ok bool)
  # context被取消或者到了deadline,返回一个关闭的channel
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

  • Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,Err 方法会返回一个错误,告知为什么 Context 被取消。

  • Err方法返回取消的错误原因,因为什么Context被取消。

  • Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的

canceler接口:

实现了下面定义的两个方法的 Context,就表明该 Context 是可取消的。

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

实现canceler接口的cancelCtx:

type cancelCtx struct {
    Context

    // 保护之后的字段
    mu       sync.Mutex
    done     chan struct{}
    children map[canceler]struct{}
    err      error
}

Done方法具体实现:

c.done 是 “懒汉式” 创建,只有调用了 Done () 方法的时候才会被创建。函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

cancel方法具体实现:

cancel() 方法的功能就是关闭 channel:close(c.done),在select中就不再是一个空的channel了;递归地取消它的所有子节点;从父节点从删除自己。最终通过关闭 channel,将取消信号传递给了它的所有子节点。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // 必须要传 err
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // 已经被其他协程取消
    }
    // 给 err 字段赋值
    c.err = err
    // 关闭 channel,通知其他协程
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }

    // 遍历它的所有子节点
    for child := range c.children {
        // 递归地取消所有子节点
        child.cancel(false, err)
    }
    // 将子节点置空
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        // 从父节点中移除自己 
        removeChild(c.Context, c)
    }
}

context的常用继承方法:

最基本的方法是context.background()

#传递父context作为参数,返回子context和取消函数用来取消context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
#多传截止时间参数,到这个时间会自动取消context,也可以用取消函数提前取消
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
同理,过了多少时间自动取消
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
生成绑定键值对的context,绑定数据可以通过context.Value访问,可以通过这种方式上下文传递数据
func WithValue(parent Context, key, val interface{}) 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)
        }
    }
}

context的使用与总结:

  • 不要把Context放在结构体中,要以参数的方式传递,parent Context一般为Background
  • 应该要把Context作为第一个参数传递给入口请求和出口请求链路上的每一个函数,放在第一位,变量名建议都统一,如ctx。
  • 给一个函数方法传递Context的时候,不要传递nil,否则在trace追踪的时候,就会断了连接
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  • Context是线程安全的,可以放心的在多个goroutine中传递
  • 可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行取消操作时,所有 goroutine 都会接收到取消信号。

 

posted @ 2020-08-10 21:39  LeeJuly  阅读(296)  评论(0编辑  收藏  举报