(转)golang中context包解读

one_zheng · · 470 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

源码剖析

context.Context 接口

context包的核心

// context 包里的方法是线程安全的,可以被多个 goroutine使用
type Context interface {
  // 如果存在,Dealline 返回Context将要关闭的时间
  Deadline() (deadline time.Time, ok bool) 

  // 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的Channel
  Done() <-chan struct{}

  // 在 Done 的 channel被closed后, Err 代表被关闭的原因
  Err() error

  // 如果存在,Value 返回与 key 相关了的值, 不存在则返回 nil
  Value(key interface{}) interface{}
}

  我们不需要手动实现这个接口,context 包已经给我们提供了两个,一个是Background(),y一个是TODO(),这两个函数都会返回一个Context的实例,只是返回的这两个实例都是 空Context.

package context

var (
    background = new(emptyCtx)
    todo = new(emptyCtx)
)

// Background returns a non-nil, empty Context.
// 该Context从不canceled,不存在values,且没有deadline时间.
// 经常用于main方法,context的初始化,和测试,或者用于最基类context用于接受
func Background() Context {
  return background
}

// TODO returns a non-nil, empty Context.
// 开发人员可以在不清楚context的使用场景和尚未调用context时使用该方法(因为它的function还未因接受context参数而实现).TODO可以由静态分析工具识别是否符合程序的正确性. 
func TODO() Context {
  return todo
}

主要结构

  cancelCtx结构体继承了Context,实现了canceler方法:

//  canceler 是一个可以被关闭的context.
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chane struct {}
}

// closedchan is a reusable closed channel.
var closechan = make(chan struct{})


func init() {
  close(closechan)
}

// *cancelCtx和*timerCtx都实现了canceler接口,实现该接口的类型都可以被直接canceled
type cancelCtx struct {
  Context
  mu sync.Mutex
  done chan struct{}  // closed by the first cancel call.
  children map[canceler]bool // set to nil by the first call
  err error // 当其被cancel时将会把err设置为非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
  return c.done
}


func (c *cancelCtx) Err() error {
  c.mu.Lock()
  defer c.mu.Unlock()
  return c.err
}

func (c *cancelCtx) String() string {
  return fmt.Sprintf("%v.WithCancel", c.Context)
}

// 核心是关闭c.done
// 同时会设置c.err = err, c.children = nil
// 依次遍历c.children,每个child分别cancel
// 如果设置了removeFromParent,则将c从其parent的
children中删除

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  if err == nil {
    panic("context: internal error: missing cancel error")
  }
  c.mu.Lock()
  if c.err != nil {
    c.mu.Unlock()
    return // already canceled
  }
  c.err = err 
  if c.done == nil {
     c.done = closechan
  } else {
      close(c.done)
  }
  for child := range c.children {
    // NOTE: acquiring the child {
      child.cancel(false, err)
 }
c.children = nil
c.mu.Unlock()

if removeFromParent {
  removeChild(c.Context, c)
}
}

timerCtx 结构继承 cancelCtx

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
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 fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))
}

func(c *timerCtx) cancel(removeFromParent bool,err error) {
  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()
}

valueCtx 结构继承 cancelCtx

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

主要方法

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 interface{}, val interface{}) Context

WithCancel 对应的是 cancelCtx ,其中,返回一个 cancelCtx ,同时返回一个 CancelFunc,CancelFunc 是 context 包中定义的一个函数类型:type CancelFunc func()。调用这个 CancelFunc 时,关闭对应的c.done,也就是让他的后代goroutine退出。

WithDeadlineWithTimeout 对应的是 timerCtx ,WithDeadline 和 WithTimeout 是相似的,WithDeadline 是设置具体的 deadline 时间,到达 deadline 的时候,后代 goroutine 退出
WithTimeout 简单粗暴,直接 return WithDeadline(parent, time.Now().Add(timeout))。
WithValue 对应 valueCtx ,WithValue 是在 Context 中设置一个 map,拿到这个 Context 以及它的后代的 goroutine 都可以拿到 map 里的值。

使用原则

  • 使用Context的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析
  • 不要把Context存放在一个结构当中,显示地传入函数.Context变量需要作为第一个参数使用,一般命名为ctx
  • 即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO
  • 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数
  • 同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的

使用示例

package main

import (
    "context"
    "fmt"
    "time"
)

// 模拟一个最小执行时间的阻塞函数
func inc(a int) int {
    res := a + 1                // 虽然我只做了一次简单的 +1 的运算,
    time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令,
    // 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B)
    return res
}

// 向外部提供的阻塞接口
// 计算 a + b, 注意 a, b 均不能为负
// 如果计算被中断, 则返回 -1
func Add(ctx context.Context, a, b int) int {
    res := 0
    for i := 0; i < a; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    for i := 0; i < b; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    return res
}

func main() {
    {
        // 使用开放的 API 计算 a+b
        a := 1
        b := 2
        timeout := 2 * time.Second
        ctx, _ := context.WithTimeout(context.Background(), timeout)
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
    {
        // 手动取消
        a := 1
        b := 2
        ctx, cancel := context.WithCancel(context.Background())
        go func() {
            time.Sleep(2 * time.Second)
            cancel() // 在调用处主动取消
        }()
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
}

输出:
Compute: 1+2, result: -1
Compute: 1+2, result: -1

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:one_zheng

查看原文:(转)golang中context包解读

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

470 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传