GO标准库巡礼-context

简述

context包的核心内容是定义了Context类型,Context可以携带DDL信号、取消信号以及基于请求的数据在函数之间传递。

context一个常见的应用场景是在服务器处理请求上。这个过程我们可能会创建很多goroutine来完成对请求的处理,一方面我们往往需要一个全局性的方式能够快速的结束所有goroutine,其中的原因可能是用户停止了请求或者说处理已经超时没必要继续处理,另一方面我们可能经常在函数调用过程中需要传递一些数据比方说用户ID、认证口令等。context包提供的核心功能就是这两个:控制子goroutine以及传递基于请求的数据

Context接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

其中,Deadline返回的是什么时候该context会被cancel,如果ok为false,表示该Context没有设置deadline

Done则是用于子goroutine接收结束信号的,一个常见的使用方式如下

func Stream(ctx context.Context, out chan<- Value) error {
      	for {
     		v, err := DoSomething(ctx)
      		if err != nil {
      			return err
      		}
      		select {
      		case <-ctx.Done()://如果收到了信号,则退出
      			return ctx.Err()
      		case out <- v:
      		}
      	}
      }

Err在Done channel未关闭之前返回nil,关闭之后,Err会返回一个非nil错误解释原因:如果是被取消则返回Canceled,如果是超时则返回DeadlineExceeded

Value则是用于我们提到的携带数据的功能,传入key则可以获取对应结果,如果没有则返回nil

使用context

从逻辑上来说,我们认为如果函数A调用函数B,那么无论B对传入的context做什么操作,都不应该影响到A,比方说B用context来设置了新的超时时间,而A不应该受到影响,因为这是B基于自己的业务场景设置的。

因此,context包通过使用复制context的方式来向下传递context。一个常见的范式如下所示

func A(ctx context.Context){
    go B(ctx)
}
func B(ctx context.Context){
    d := time.Now().Add(50 * time.Millisecond)
    context, cancel := ctx.WithDeadline(ctx, d)//这里自行设置了deadline
    for{
   	select <-context.Done()://注意这里子goroutine通过监听done channel来配合关闭操作
        fmt.Println(timeout);
    default:
    		   	//do something
    }
}

从宏观层面上来说,context的使用会更像是一棵树,每一个树中的子节点都意味着父节点创建的一个新的goroutine,而从根节点不断往下,则是对请求的不断处理加工。如下图所示(极客时间《go语言核心36讲》)

img

于是问题就变成了,如果创建根节点,如何创建子节点?

  • 如何创建根context?

    我们可以调用context.Background()来获取一个非nil的空context,该context不会被取消(所以Donechannel永远收不到值)没有deadline也没有携带值。它是所有context的起点,一般应该是在主函数、初始化、请求处理的起点上调用创建

  • 如何创建子context?

    • 希望赋予关闭子goroutine的能力

      我们可以使用WithCancel函数,函数定义为

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

      父goroutine可以通过调用返回的cancel函数来发出信号。监听Done channel的子goroutine可以获取信号并自行关闭。官方提供的例子如下

      gen := func(ctx context.Context) <-chan int {
      		dst := make(chan int)
      		n := 1
      		go func() {
      			for {
      				select {
      				case <-ctx.Done():
      					return // 获取到信号并退出,从而关闭goroutine,避免泄露
      				case dst <- n:
      					n++
      				}
      			}
      		}()
      		return dst
      	}
      
      	ctx, cancel := context.WithCancel(context.Background())
      	defer cancel() // 这里确保调用cancel
      
      	for n := range gen(ctx) {
      		fmt.Println(n)
      		if n == 5 {
      			break
      		}
      	}
      
    • 希望设置超时时间

      我们可以使用WithDeadline或者WithTimeout。二者的区别是前者指定明确的时间点,后者指定的若干时间后。函数定义如下

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

      注意同样可以用返回的cancel函数来确保关闭

    • 希望传递值

      我们可以使用WithValue来实现这个目的,函数定义如下

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

更好的使用context

context应该仅仅只被使用在两种场景

  • 设置关闭以及超时

  • 在API之间传递数据,尤其是需要通过第三方框架的时候

    比方说某函数调用为 A->第三方函数B->C。 C需要使用到特定值如验证信息等,但是尴尬的是第三方函数B无从知晓C需要什么。这时候context就起到传递的作用了,B只需要向C传递从A获取的context即可。

但是在实际使用的时候,人们常常会滥用context比方说用其存储可选参数,或者说以字符串等数值作为键

误区一:可选参数?

下面的代码来自于某开源项目的截取片段,如下图所示,该函数试图从ctx中逐步提取所有可能的参数并返回结果。

func applyContextSettings(ctx context.Context, req Request) contextSettings {
	result := contextSettings{}
	if ctx == nil {
		return result
	}
	// Details
	if v := ctx.Value(keyDetails); v != nil {
		if details, ok := v.(bool); ok {
			req.SetQuery("details", strconv.FormatBool(details))
		}
	}
    //...
    
}

这样做的问题在于,context存储值需要调用WithValue来创建新的context,而可选参数一般可能有十几数十个,这意味着中间需要复制大量的context(如下图所示),不仅仅影响性能,也会导致GC性能下降。

//该代码复制了三次context
driver.WithQueryCache(driver.WithQueryCount(driver.WithQueryBatchSize(nil, 2))),

context存储的值应该是必要选项以及request本身的数据。如果我们真的需要实现可选参数,可以参考这篇文章

误区二:基础类型为key?

一个更为常见的错误是以字符串等为键。由于context会在API调用过程中传递,你无法知道其他package是如何定义的,这导致使用基础类型会不经意间出现键重复而导致严重问题。

一个正确的做法应该是:设置非导出类型为键(从而不会在包之间错误的共享键值对),用函数封装该包对context的使用。

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address.  Its value of zero is
// arbitrary.  If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
	return context.WithValue(ctx, userIPKey, userIP)
}

// FromContext extracts the user IP address from ctx, if present.
func FromContext(ctx context.Context) (net.IP, bool) {
	// ctx.Value returns nil if ctx has no value for the key;
	// the net.IP type assertion returns ok=false for nil.
	userIP, ok := ctx.Value(userIPKey).(net.IP)
	return userIP, ok
}

总结

context包是实践的产物,其控制子goroutine的功能来自于pipeline模式对于控制子goroutine的经典方式,它虽然可以传递数据,但是一定不能被滥用

发布了31 篇原创文章 · 获赞 32 · 访问量 726

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/105119201