context包定义了Context类型,这个类型在API边界即进程中传递截止日期、同步信号,请求值等相关信息。

1. 对context包的介绍

在服务器的传入请求中应包含context,而对服务器的传出调用应接收一个context。它们之间的调用链必须包含context,或是衍生的WithCancel, WithDeadline, WithTimeout, WithValue。当一个WithCancel Context被“cancel”,那么当前context所派生的所有context也都将被取消。

WithCancel, WithDeadline, WithTimeout接收一个Context对象(父对象),并返回其父对象的一个携带有cancel/deadline/timeout的一个拷贝(子对象)。调用CancelFunc会取消其子对象及子对象的子对象等,删除父对象对子对象的引用,并停止所有关联的计时器。未能调用CancelFunc将造成父对象结束前或计时器被触发前子对象的泄露。使用go vet工具可以检查所有控制流路径上是否都使用了CancelFunc

使用context的程序应遵循以下规则,以使各个包之间的接口保持一致,并启用静态分析工具来检查上下文传播:

  1. 不要将context存储在结构类型中,而是将context明确传递给需要它的每个函数。Context应该是第一个函数,通常命名为ctx
func DoSomething(ctx context.Context, arg Arg) error {
    // ...use ctx...
}
  1. 不要传递一个值为nil的context,即使一个函数允许这样做。如果你不确定Context的作用那就请传递context.TODO
  2. 只在进程和API间传递请求范围数据时使用context值,不要用于将可选参数传递给函数。
  3. 同样的Context可以传递给运行在不同goroutine中的函数,Context是线程安全的。

2. Context接口

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

Context是一个接口,其定义非常的简单,只包含4个方法:

  • Done() <-chan struct{} Done()方法将一个channel作为取消信号返回给持有context的函数,当该channel被关闭(即Done()被调用),这些函数应该立即停止其工作并返回。

  • Err() error Err()返回一个Error,说明为什么取消context。如果Done()没有被调用,那么Err返回nil。

  • Deadline() (deadline time.Time, ok bool) Deadline()方法返回持有这个context的函数的预期结束时间。如果并没有设置deadline,那么返回的ok将被设置为false

  • Value(key interface{}) interface{} Value()方法返回与此context关联的key,如果没有与key对应的值那么返回nil。 key可以是任何支持比较的类型,为了避免冲突,应将key定义为不可导出的。 示例:

    package user
    import "context"
    
    type User struct{...}
    
    type key int
    
    var userKey key
    
    func NewContext(ctx context.Context, u *User) context.Context {
        return context.WithValue(ctx, userKey, u)
    }
    
    func FromContext(ctx context.Context)(*User, bool){
        u, ok := ctx.Value(userKey).(*User)
        return u, ok
    }
    

3. Context构造

构造一个context对象有两种方法。

func Background() Context

func TODO() Context

上面两个方法都会返回一个非nil,非空的Context对象。Background()一般用于构造出最初的Context,所有的Context都派生自它。TODO()用当传入的方法不确定是哪种类型的Context时,为了避免Context参数为nil而初始化的Context

构造出Context对象后,我们就可以使用WithCancel, WithDeadline, WithTimeout, WithValue来进一步的设置Context,构造出的Context都派生自Background或是TODO 20210523145028

4. context.With…

4.1 context.WithCancel()

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

WithCancel接收一个父context并返回该父context的一个持有Done channel的子context和一个cancel方法,当cancel方法被调用时或是父contextDone channel被关闭时,当前contextDone channel将被关闭。

WithCancel常被用于通知goroutine退出。

func fibonacci(c chan int, ctx context.Context)  {
	x, y := 0, 1
	for{
		select {
		case c <- x:
			x, y = y, x+y
		case <-ctx.Done():
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	defer func() {
		time.Sleep(time.Second)
		fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
	}()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	c := make(chan int)

	go fibonacci(c, ctx)

	for i := 0; i < 10; i++{
		fmt.Println(<- c)
	}
}

4.2 context.WithValue()

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

WithValue方法接收一个父context,以及一个键值对,返回一个包含这个键值对的子context。可使用context.Value(key)方法取出其中保存的值。

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	valueCtx := context.WithValue(ctx, key, "add value")

	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	cancel()

	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//get value
			fmt.Println(ctx.Value(key), "is cancel")

			return
		default:
			//get value
			fmt.Println(ctx.Value(key), "int goroutine")

			time.Sleep(2 * time.Second)
		}
	}
}

4.3 context.WithDeadline()

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

WithDeadline方法接收一个父context和一个截止时间d,返回子context并为其设置一个不晚于d的截止时间。如果父context的截止时间已经早于d, 那么WithDeadline在语义上与父context相同。当以下三种情况发生时:1.截止时间到来,2.cancelFunc被调用,3.父contextDone channel被关闭,当前contextDone channel将被关闭。

func main() {
	d := time.Now().Add(5 * time.Second) // 5秒后到期
	ctx, cancel := context.WithDeadline(context.Background(), d)
	defer cancel() // 一个好的习惯是调用cancel()以防止goroutine泄露

	go doSomething(ctx)

	time.Sleep(10 * time.Second)
}

func doSomething(ctx context.Context) {
	for {
		select {
		case <-time.After(time.Second):
			fmt.Println("I'm doing some funny things")
		case <-ctx.Done():  // 5秒时到期
			fmt.Println(ctx.Err())
			return
		}
	}
}
I'm doing some funny things
I'm doing some funny things
I'm doing some funny things
I'm doing some funny things
context deadline exceeded

4.4 context.WithTimeout()

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

从其实现就可以看出,WithTimeout只是对WithDeadline的进一步封装,这层封装为context设置了一个截止时间,也就是规定了其超时时间。

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(2*time.Second)) // 超过两秒就退出,不再continue
	defer cancel() // 一个好的习惯是调用cancel()以防止goroutine泄露

	go seek(ctx)

	time.Sleep(5 * time.Second)
}

func seek(ctx context.Context) {
	for {
		select {
		case <-time.After(time.Second):
			fmt.Println("I'm looking for something")
		case <-ctx.Done():
			fmt.Println(ctx.Err())
			return
		}
	}
}

5. context包的其他函数

5.1 context.String()

实现fmt.Stringer接口,用于打印context

func main() {
	backgroundCtx := context.Background()
	fmt.Println(backgroundCtx)

	withValueCtx := context.WithValue(backgroundCtx, "one", 1)
	fmt.Println(withValueCtx)

	withCancelCtx, cancel := context.WithCancel(withValueCtx)
	defer cancel()
	fmt.Println(withCancelCtx)

	d := time.Now().Add(2 * time.Second)
	withDeadlineCtx, cancel := context.WithDeadline(withCancelCtx, d)
	defer cancel()
	fmt.Println(withDeadlineCtx)

	withTimeoutCtx, cancel := context.WithTimeout(withDeadlineCtx, time.Duration(2 * time.Second))
	defer cancel()
	fmt.Println(withTimeoutCtx)
}
context.Background
context.Background.WithValue(type string, val <not Stringer>)
context.Background.WithValue(type string, val <not Stringer>).WithCancel
context.Background.WithValue(type string, val <not Stringer>).WithCancel.WithDeadline(2021-05-23 15:49:31.513587636 +0800 CST m=+2.000147913 [1.999897372s])
context.Background.WithValue(type string, val <not Stringer>).WithCancel.WithDeadline(2021-05-23 15:49:31.513587636 +0800 CST m=+2.000147913 [1.999881255s]).WithCancel

5.2 context.Value()

用于从WithValue context中根据key取值

func main() {
	ctx := context.WithValue(context.Background(), "one", 1)
	fmt.Println(ctx.Value("one"))
}

值取出后context不会删除它,可重复取值,对一个不存在的key取值会返回nil。

context

Reference

Package context

Go Concurrency Patterns: Context

Golang Context深入理解

Golang Context 原理与实战

Go 语言设计与实现