Context简介:

     Context由Google官方开发,在1.7版本引入, 在Go服务器程序中,每个请求都会有一个goroutine去处理。然而,处理程序往往还需要创建额外的goroutine去访问后端资源,比如数据库、RPC服务等。由于这些goroutine都是在处理同一个请求,所以它们往往需要访问一些共享的资源,比如用户身份信息、认证token、请求截止时间等。而且如果请求超时或者被取消后,所有的goroutine都应该马上退出并且释放相关的资源。Go提供了一个Context包,也就是上下文,来控制它们达到目的,本文我们来介绍Context包的基本使用方法。

Context使用:

        在请求处理的过程中,会调用各层的函数,每层的函数会创建自己的routine,构成了一个routine树, 如果由Context来控制上下文,context也应该反映并实现成一棵树。

golang grpc 发布订阅_golang grpc 发布订阅


    根节点

        要创建context树,第一步是要有一个根结点。context.Background函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间

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
}
type emptyCtx int

其中emptyCtx  实现了Context接口。

    子节点

    context包为我们提供了以下函数:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context

    这四个函数的第一个参数都是父context,返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收的函数参数保存子节点的一些状态值,然后就可以将它传递给下层的routine了。

package main

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

func Println(ctx context.Context, a, b int) {
	for {
		fmt.Println(a + b)
		a, b = a+1, b+1
		select {
		case <-ctx.Done():
			fmt.Println("程序结束")
			return
		default:
		}
	}
}

func main() {
	{
		// 超时取消
		a := 1
		b := 2
		timeout := 2 * time.Second
		ctxBg := context.Background()
		ctx, _ := context.WithTimeout(ctxBg, timeout)
		Println(ctx, a, b)

		time.Sleep(2 * time.Second) // 等待时候还会继续输出
	}
	{
		// 手动取消
		a := 1
		b := 2
		ctx, cancelCtx := context.WithCancel(context.Background())
		go func() {
			time.Sleep(2 * time.Second)
			cancelCtx() // 在调用处主动取消
		}()
		Println(ctx, a, b)

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

有一个大胆的想法,如果改动手动取消代码如下:

{
		// 手动取消
		a := 1
		b := 2
		ctx, _ := context.WithCancel(context.Background())
		go func() {
			time.Sleep(2 * time.Second)
			// cancelCtx() // 注释了主动的取消代码
		}()
		Println(ctx, a, b)

		time.Sleep(2 * time.Second)
	}

程序会有怎样进行呢?动手试试吧。 

再来看看WithTimeout的源码吧

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

要注意的WithDeadline的第二个参数和WithCancel的区别。

验证超时

func doTimeOutStuff(ctx context.Context) {
	for {
		time.Sleep(1 * time.Second)
		if deadline, ok := ctx.Deadline(); ok {
			fmt.Println("deadline set : ", deadline.Unix())
			if time.Now().After(deadline) {
				log.Fatal("错误:", ctx.Err().Error())
			}
		}

		select {
		case <-ctx.Done():
			fmt.Println("Done")
			return
		default:
			fmt.Println("working")
		}
	}
}
func main() {
	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
	fmt.Println("当前时间: ", time.Now().Unix())
	fmt.Println("超时时间: 5秒")
	go doTimeOutStuff(ctx)

	time.Sleep(10 * time.Second)

}

验证是否超时做日志记录处理,或者根据业务做出相应的提示。 在<-ctx.Done() 做好资源的控制,必满资源浪费。

返回值CancelFunc

    调用CancelFunc, 会取消child以及child生成的context,取出父context对这个child的引用,停止相关的计数器。

WithValue

package main

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

var key string = "name"

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx := context.WithValue(ctx, key, "【监控1】")
	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}
func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//取出值
			fmt.Println(ctx.Value(key), "监控退出,停止了...")
			return
		default:
			//取出值
			fmt.Println(ctx.Value(key), "goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

Context 使用原则

  1. 不要把Context放在结构体中,要以参数的方式传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context是线程安全的,可以放心的在多个goroutine中传递