一周一package之context包

367 阅读7分钟

context 包是用来干啥的

这包是用来干啥的可能直接看官方的文档最合适:

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

从这里看出 context包主要用来在跨api,goroutine传递消息(deadline, cancellation signals, and other scoped values)。在golang 一般业务逻辑和底层是分开的,他们属于两个goroutine,context可以个更优雅的在他们之间传播消息。

可以简单看下Context 包含的内容。

type Context interface {
	// 获取一个有deadline contxt 的过期时间,如果该context 没有deadline
    // ok 为false
	Deadline() (deadline time.Time, ok bool)
	
    // 一个只读 channel,等待 context结束(被cancel,达到deadline)
	Done() <-chan struct{}
    // 如果Done 没有比close,返回nil,否则返回被close的原因
    // canceled for context was canceled
    // DeadlineExceeded for context deadline passed
	Err() error
  
    // 携带的数据
	Value(key interface{}) interface{}
}

WithCancel

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

返回父 context的一个拷贝,以下两种情况下Done channel 会被关闭

  1. 返回的cancel方法被主动调用
  2. 父context的Done channel被关闭

由于Context 是跨goroutine 之间传递的,这里只要父进程或者父进程的父进程被cancel了,新的context 就会收到 Done的消息。下面来看一下官方的一个例子:

package main

import (
	"context"
	"fmt"
)
func main() {
	// gen generates integers in a separate goroutine and
	// sends them to the returned channel.
	// The callers of gen need to cancel the context once
	// they are done consuming generated integers not to leak
	// the internal goroutine started by gen.
	gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done(): // 等待 context 被cancel
					return // returning not to leak the goroutine
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // cancel when we are finished consuming integers

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
                      // 跳出for 循环结束,之后会执行上面的defer cancel,
                       // 然后在gen 就会收到context Done channel被关闭的通知
			break 		
                }
	}
}

WithDeadline

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

返回父 Context 的一个拷贝,在以下几种情况下Done channel会被关闭

  1. deadline 过期
  2. cancel 函数被调用
  3. 父 Context 的Done channel 被关闭

这里需要注意的是如果 父 Context 本身也是一个 WithDeadline的情况

  1. 如果 父 Context 的 deadline 比d要早直接返回父 Context
  2. 如果 d已经过期,设置子context的error 为 DeadlineExceeded

WithTimeout

WithDeadline的变体, 等于 WithDeadline(parent, time.Now().Add(timeout))

WithValue

在context 中附加(key,value) pair

常见使用方法

WithCancel

在处理http请求的时候,如果这个请求在业务逻辑处理完成之前,底层socket被关闭的时候取消上面的业务逻辑(以此再去做业务逻辑已经没啥意义了)。

// 请求处理函数
func handler(r *http.Request, w *http.ResponseWriter) {
	ctx, cancel := newCtx2(context.TODO(), w)
	defer cancel()
	doSomeThing(ctx, r) // 业务逻辑处理函数
}
// 构建一个新的 context
func newCtx2(ctx context.Context, w *http.ResponseWriter) (context.Context, context.CancelFunc) {
	var cancel context.CancelFunc
	if cn, ok := w.(http.CloseNotifier); ok {
        // 底层socket 被关闭
		ctx, cancel = context.WithCancel(ctx)
		notifyChan := cn.CloseNotify()
		go func() {
			select {
			case <-notifyChan:
				cancel() // cancel context
			case <-ctx.Done():
			}
		}()
	} else {
		cancel = nilCancel
	}
	return ctx, cancel
}
func nilCancel() {}

WithTimeout

处理 http client 请求超时处理, 这里请求 www.google.com,如果超时返回 ctx.Err() 返回 deadline exceed,否则打印success 信息

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.TODO(), time.Millisecond*2) 
	defer cancel()
	done := make(chan struct{})
	go doRequest(ctx, done) // 业务请求
	select {
	case <-ctx.Done():// 超时
		d, _ := ctx.Deadline()
		fmt.Println("err = ", ctx.Err(), d)
	case <-done: // 正常处理结束
		fmt.Println("successful handle the request")
	}
}
func doRequest(ctx context.Context, done chan struct{}) {
	req, _ := http.NewRequest("GET", "http://www.google.com", nil)
	req.WithContext(ctx)
	http.DefaultClient.Do(req)
	close(done)
}

这里只罗列了这两种简单的用法,跨goroutine, 快api的只要有cancel, 处理timeout逻辑的都可以使用这种逻辑去处理。

内部实现原理

context 包的实现主要依赖 timerCtx 和 cancelCtx 这两个结构体, 这里也主要分析他俩的代码。

Cancel Context

下面是内部 concelCtx的定义

type cancelCtx struct {
	Context // 父 Context
	mu       sync.Mutex            // protects following fields
	done     chan struct{}         // created lazily, closed by first cancel call
    // 存放该 context 派生出来的子context
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}

下面就以构建一个 WithCancel 的context 来看下具体的实现代码

// 构建一个cancel ctx 
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil { // 确保父 contxt 不为空
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)  // 产生一个上面定义的cancelCtx
	propagateCancel(parent, &c) // 将上面派生出来的 contxt 放到父 context 对应的 children 字段中
	return &c, func() { c.cancel(true, Canceled) }
}

// 给派生出来的 context 安排一个合适的位置, 以便父 context 被取消的时候能够通知到children
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}
	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

    // 查找 parent 对应的 cancelCtx,如果父 context 是一个可 cancel 的context,将 child 放到 parent.children 中(父context cancel的时候需要通知到子 context)
	if p, ok := parentCancelCtx(parent); ok { // 
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{} // 将 child 添加到
		}
		p.mu.Unlock()
	} else { // 父 context 不是一个cancel context
		atomic.AddInt32(&goroutines, +1)
        // 在这里可以看到 如果父 context 不是一个可 cancel 的context的时候,是单独启一个goroutine 等等 context 结束的。这也是为啥一定要调用 defer cancel()的目的,不然就会发生goroutine 泄漏。
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
    }
}

下面来看下一个concel 的处理过程

// CancelFunc 的原始定义
type CancelFunc func()
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's 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 { // 第一次调用 cancel 之后 c.err 就会设置为 err,
这里如果 c.err已经被设置了就说明,该context 已经被cancel了
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done) // 通知单独起的 goroutine, 该context 被cancel
	}
    // 通知所有的派生 context,这里递归的调用cancel 每个一派生的context
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

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

至此 cancelContext 的代码也就分析完了,整体过程是这样的 创建:

  1. 第一次需要派生一个cancel context的时候会起一个goouritne去等到 Done 的channel
  2. 如果已经是一个 cancel context 了则将将 派生出的 context 放到 parent的children中

cancel:

a. 如果已经被cancel 直接返回

b. close(c.done) 通知上面创建中a 起的goroutine

c. cancel 所有的children, 以及 children cancel 自己的children

Deadline Context

把上面的 cancel context 坑透之后再去看 deadline context 就简单多了, 下面一个deadline contxt 底层 timerCtx 的定义:

type timerCtx struct {
	cancelCtx // timer context的好多方法都会委托给cancelCtx
	timer *time.Timer // 对应 timer
	deadline time.Time // deadline 时间点
}

下面来看下一个timerContext 的构造过程:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
    // parent 是一个deadlinie context 并且,parent.deadline > d
    // 直接返回 父 context(因为父 context 被取消的时候,对应的所有子孙都会被取消,所以 child.deadline > parent.deadline 就没意义了)
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
    // 构造 timerCtx
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
    // 和cancel context 一样,给新派生出来的context 安排一个合适的位置,以便父 context 被cancel 的时候通知到个子 context 
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 { // 已经过期
        // 注意一下由于上面已经c 放到 parent.children 或者,单独其一个goroutine了, 这里需要回退掉
		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 { // 没有被cancel 或者deadline excedd
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded) // dur 之后自动cancel
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

下面也看下 deadline context 被取消的过程

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err) // 委托给 cancelCtx
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
    // 取消timer
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

避坑指南

给你一把瑞士军刀,如果用不好可能很容易伤到自己,context 也是,在业务中有些东西也要避免滥用。

  1. 如果不是需要跨多个api之间传递的参数就不要带到context 中, 能直接使用参数形式传递的就尽量使用参数形式传递。
  2. api里面不要保存传进来的context,只应该让他在api之间流转
  3. 不要传递一个nil 给context
  4. 记得 **defer cancel() **去释放资源
  5. context 按照约定放到函数的第一个参数
func DoSomething(ctx context.Context, arg Arg) error {
 		// ... use ctx ...
}
  1. 一定要理解context中 chain的概念,一个context可以不断的派生出contxt, 只要一个conext 被cancel,他派生出来的子,子的子 等等都会被关闭。

参考:

  1. golang.org/pkg/context…
  2. blog.golang.org/context
  3. blog.cloudflare.com/the-complet…