Go Context

137 阅读2分钟

走去看看 Context 里面有什么

Cotext interface

首先,我们来看一下Context的接口中有哪些方法:

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}

Deadline: Deadline 返回 context 的截至时间,当deadline 没有设置时, ok 设置为false

Done: 返回一个Channel对象, 在Context 被取消时, Channel 会被关闭,如果没有被取消,可能会返回 nil

Err: 如果 Done 没有被关闭了, Err返回 nil;如果 Done 被关闭了, Err 会返回被 close 的原因

Value:返回 context 中指定 key 的 value,如果 key 没有被指定则返回 nil

CancelFunc()

type CancelFunc func()

CancelFunc用来直接关闭一个 context, 一个 CancelFunc 可以被多个 goroutine 同时调用,再第一次调用后,后面调用它不再做任何事情。

Context 的起源

func Background()

Background 返回一个 non-nil, 空的 Contex, 它将永远不会被取消掉,没有 values 和 deadline,它被当作顶层上下文使用,通常被 main 函数使用,用于初始化和测试。

func TODO()

TODO 本质实现是和Background() 是一样的,官方给出的回答是在你不清楚要用哪一个 Context 的时候或者它没有需要到被使用的时候使用。

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)
func Background() Context {
	return background
}
func TODO() Context {
	return todo
}

创建Context的四种方法

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel 创建一个 parent 的副本,调用 CancelFunc 时,当前 context 会关闭。当 parent 关闭时时,当前 context 会跟着关闭。

func TestWithCancel(test *testing.T) {
	ctx1, cancel1 := context.WithCancel(context.Background())
	defer cancel1()
	ctx2, cancel2 := context.WithCancel(ctx1)
	defer cancel2()

	fmt.Println("ctx1:", ctx1.Err())
	fmt.Println("ctx2:", ctx2.Err())
	cancel1()
	fmt.Println("ctx1:", ctx1.Err())
	fmt.Println("ctx2:", ctx2.Err())
}

//output
ctx1: <nil>
ctx2: <nil>
ctx1: context canceled
ctx2: context canceled

首先我们创建 ctx1, 创建以 ctx1 为父 context ctx2, 输出 ctx1.Err(), ctx2.Err() ,输出为 nil ,当前2个cxt 都没有被关闭,当我们 cancel1() 关闭 ctx1 时,输出发现 2 个 cxt 都已经被关闭。

方法实现:

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) }
}

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

newCancelCtx 返回一个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
}

propagateCancel(parent, &c) 这个方法将父 context 和 子context做关联,子 context 保存在children 里面。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline 返回一个 parent 的副本,并设置一个不晚于d的时间, 如果 parent 的截至时间比 d 早,则以 parent 的时间为准。

func TestWithDeadline(test *testing.T) {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
	defer cancel()
	for {
		select {
		case <-ctx.Done():
			fmt.Println(ctx.Err())
			return
		case <-time.After(1 * time.Second):
			fmt.Println("overslept")
		}
	}
}

//output
overslept
overslept
overslept
overslept
context deadline exceeded

首先,我们创建了一个5秒钟后结束 Context, 在select语句种, 我们接收到 ctx Done 关闭的信号输出Err的信息并退出,每过一秒钟输出overslept。

方法实现:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	// 如果parent的截至时间更早,直接创建parent的副本
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
  1. 如果 parent 的是截至时间比当前 context 早,则直接用 WithCancel() 创建 parent 的副本
  2. propagateCancel() 绑定父子关系
  3. dur < 0 如果已经超过时间取消
  4. 最后执行一个定时任务,到截至时间后取消

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

WithTimeout 本身调用了WithDeadline,只不过一个参数是超时时间,一个参数是截至时间

func WithValue(parent Context, key, val interface{}) Context

ctx := context.Background()
ctx = context.WithValue(ctx, "key1", "123")
fmt.Println(ctx.Value("key1"))

如果 key 不存在, ctx 会从 parent 中去寻找。

注意事项:

当 parent 的 context 被关闭之后,所属的子context 都会被关闭

参考文献:

pkg.go.dev/context#sec…