TOC
Context作为Golang的上下文传递机制,其提供了丰富功能,接下来将介绍其原理和使用。
在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方法有四种方式
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
}
(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
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。