zl程序教程

您现在的位置是:首页 >  工具

当前栏目

Golang Context学习

学习Golang Context
2023-06-13 09:11:20 时间

TOC

Context作为Golang的上下文传递机制,其提供了丰富功能,接下来将介绍其原理和使用。

Context概念和创建

在Golang中,Context就是携带了超时时间、取消信号和值的一种结构。

Context接口

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

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

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

创建Context方法有四种方式

  • WithCancel函数,是将父节点复制到子节点,并且返回一个额外的CancelFunc函数类型变量,该函数类型的定义为:type CancelFunc func()。调用 CancelFunc 将撤销对应的子context对象。在父goroutine中,通过 WithCancel 可以创建子节点的 Context, 还获得了子goroutine的控制权,一旦执行了 CancelFunc函数,子节点Context就结束了,子节点需要如下代码来判断是否已经结束,并退出goroutine
  • WithDeadline函数作用和WithCancel差不多,也是将父节点复制到子节点,但是其过期时间是由deadline和parent的过期时间共同决定。当parent的过期时间早于deadline时,返回的过期时间与parent的过期时间相同。父节点过期时,所有的子孙节点必须同时关闭。
  • WithTimeout函数和WithDeadline类似,只不过,他传入的是从现在开始Context剩余的生命时长。他们都同样也都返回了所创建的子Context的控制权,一个CancelFunc类型的函数变量。当顶层的Request请求函数结束时,我们可以cancel掉某个context,而子孙的goroutine根据select ctx.Done()来判断结束。
  • WithValue函数,返回parent的一个副本,调用该副本的Value(key) 方法将得到value。这样,我们不仅将根节点原有的值保留了, 还在子孙节点中加入了新的值;注意如果存在key相同,则会覆盖。在使用value查找key对应的值时,如果没找到,就会从父上下文中查找,直某个父上下文中返回nil或者找到对应的值。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
func WithValue(parent Context, key, val interface{}) Context {}

从context的源码可以看出,context是immutable,即不可变的,即使采用WithValue为context设置一个key value,也是会派生出一个新的context,并将该key value绑定在该新context上。

在key的使用上,必须是可以比较的key。虽然可以使用基本数据类型,如string、int等其他内置的基本类型作为key,但是为了防止key碰撞,不建议这么使用。最好的实践方式就是为key定义单独的类型,这个类型可以是string、int等基本类型,不过一般建议是struct,空的结构体不占用空间。

为了更加严格的约束key的使用,最好的方式是将key作为私有变量,采用Getter和Setter的方式来操作key value。

type ctxKey struct{}

var ctxReqID = ctxKey{}

func WithReqID(ctx context.Context, reqID string) context.Context {
  return context.WithValue(ctx, ctxReqID, reqID)
}

func GetReqID(ctx context.Context) (string, bool) {
  reqID, exist := ctx.Value(ctx, ctxReqID)
  return reqID, exist
}

Context理解

(1)context作为Golang中一个struct,并没有和Goroutine有着紧密的联系,仅作为一个普通的对象,用于传递字段和设置超时。

(2)goroutine中没有方法可以像java语言直接获取当前协程的上下文context

(3)当子协程直接使用父协程的context时,并不会直接创建一个子context,只有当父协程创建一个子context显示传递给子协程时,才会形成子协程树结构,测试代码如下:

func TestContextScope(t *testing.T) {
    ctx := context.Background()
    fmt.Println("parent context:", &ctx)
    fmt.Println("parent gid:", GetGid())
    childCtx, cancel := context.WithCancel(ctx)
    fmt.Println("child context:", &childCtx)
    cancel()
    fmt.Println("child after cancel:", &childCtx)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("goroutine child gid:", GetGid())
        fmt.Println("goroutine child context:", &childCtx)
        newCtx := context.Background()
        fmt.Println("goroutine new context:", &newCtx)

    }()
    wg.Wait()
}

(4)打印日志需要使用traceid,而该信息一般放在context,此时若不想层层传递context,只能在一个集中的地方维护协程号和traceid等对应关系,且放入traceid到context的协程又创建了子协程,而子协程有需要打印日志时,此时还需要维护父协程和子协程的关系,在打印日志时根据协程号来查询对应的traceid,这种方式,在获取协程号和维护父子协程关系并查找的开销比较大,使用context层层传递traceid信息更加高效。官方并未直接提供获取协程号的方法,可以自行获取协程号,方法如下:

func GetGid() uint64 {
    b := make([]byte, 64)
    b = b[:runtime.Stack(b, false)]
    b = bytes.TrimPrefix(b, []byte("goroutine "))
    b = b[:bytes.IndexByte(b, ' ')]
    n, err := strconv.ParseUint(string(b), 10, 64)
    if err != nil {
        panic(err)
    }
    return n
}