context上下文

1. context简介

contextgoroutine
WithCancelWithDeadlineWithTimeoutWithValue

简而言之:context的作用就是一种多Goroutine管理机制。

contextcontext

context包的主要内容可以被概括为1个接口4个具体实现6个函数

在这里插入图片描述

2. context接口

context
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
DeadlineDoneErrValue

3. emptyCtx、cancelCtx、timerCtx和valueCtx

emptyCtx

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
emptyCtx

cancelCtx

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
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
}
cancelCtxcontextdonechildrenerr

timerCtx

// 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
}
timerCtx
timerCtxtimerCtx

valueCtx

// 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{}
}
valueCtxvalueCtxvalueCtx

4. Background() 和 TODO()

对于goroutine,他们的创建和调用关系总是像层层调用进行的,就像一个树状结构,而更靠顶部的context应该有办法主动关闭下属的goroutine的执行。为了实现这种关系,context也是一个树状结构,叶子节点总是由根节点衍生出来的。

要创建context树,第一步应该得到根节点,context.Background()函数的返回值就是根节点。

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

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}

这两个函数函数内部都会创建一个emptyCtx。

Background()

Background()函数返回的变量结构:

在这里插入图片描述

TODO()

5. WithCancel()、WithDeadline()、WithTimeout()和WithValue()

有了根节点,就可以创建子孙节点了,context包提供了一系列方法来创建他们:

// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()

怎么通过context传递改变后的状态呢?
在父goroutine中可以通过Withxx方法获取一个cancel方法,从而获得了操作子context的权力。

WithCancel()

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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) }
}
WithCancel

我们来了解一个利用context结束goroutine的demo。

package main

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

/*
	创建一个通道chan,启动goroutine
	for循环储存数据
 */
func gen(ctx context.Context) <-chan int {
	dst := make(chan int)
	n := 1
	go func() {
		for  {
			select {
				case <- ctx.Done() :
					return  //return结束该goroutine,防止泄露
				case dst <- n:
					n++
			}
		}
	}()
	return dst
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//当取数据n == 5的时候,执行defer cancel操作
	defer cancel()

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
 	}
	 time.Sleep(time.Second * 5)
}

运行结果:

1
2
3
4
5
gengen

WithDeadline()

// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		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) }
}
WithDeadline
package main

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

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
	// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
	defer cancel()

	select {
		case <- time.After(1 * time.Second):
			fmt.Println("overslept")
		case <- ctx.Done():
		fmt.Println(ctx.Err())
	}
}

运行结果:

context deadline exceeded
deadlinecontext.WithDeadline(context.Background(), d)selectoverslept
ctx.Done()contextctx.Err()

WithTimeout

// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// 	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// 		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// 		defer cancel()  // releases resources if slowOperation completes before timeout elapses
// 		return slowOperation(ctx)
// 	}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout

当顶层的Request请求函数结束后,我们就可以cancel掉某个context,从而层层Goroutine根据判断cxt.Done()来结束。

package main

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
	for {
		fmt.Println("db connecting ...")
		time.Sleep(time.Millisecond * 10) //假设正常连接数据库耗时10毫秒
		select {
			case <- ctx.Done():  //50毫秒后自动调用
				break LOOP
			default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	//设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond * 50)
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel()  //通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

运行结果:

db connecting ...
db connecting ...
db connecting ...
db connecting ...
db connecting ...
worker done!
over

WithValue()

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}
WithValue
package main

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

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
	key := TraceCode("TRACE_CODE")
	traceCode, ok := ctx.Value(key).(string)  //在子goroutine中获取trace code
	if !ok {
		fmt.Println("invalid trace code")
	}
LOOP:
	for {
		fmt.Printf("worker, trace code:%s\n", traceCode)
		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
		select {
		case <-ctx.Done(): // 50毫秒后自动调用
			break LOOP
		default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	// 设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
	// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

运行结果:

worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker done!
over

6. 使用context小结

小结:

  • context包通过构建树形关系的context,来达到上一层goroutine对下一层goroutine的控制。对于处理一个request请求操作,需要通过goroutine来层层控制goroutine,以及传递一些变量来共享。
  • context变量的请求周期一般为一个请求的处理周期。即针对一个请求创建context对象;在请求处理结束后,撤销此ctx变量,释放资源。
  • 每创建一个goroutine,要不将原有context传递给子goroutine,要么创建一个子context传递给goroutine.
  • Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
  • 当通过父
    Context对象创建子Context时,可以同时获得子Context的撤销函数,这样父goroutine就获得了子goroutine的撤销权。

注意事项:

  • 推荐以参数的方式显示传递Context。
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()。
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数。
  • Context是线程安全的,可以放心的在多个goroutine中传递。