当前位置 博文首页 > r1chard:一种获取context中keys和values的高效方法 | golang

    r1chard:一种获取context中keys和values的高效方法 | golang

    作者:r1chard 时间:2021-01-23 20:41

    我们知道,在 golang 中的 context 是一个非常重要的包,保存了代码活动的上下文。我们经常使用 WithValue() 这个方法,来往 context 中 传递一些 key value 数据。
    如果我们想拿到 context 中所有的 key and value 或者在不知道 key 的情况想获得value 要怎么做呢?这里提供一个比较hacker的实现给大家参考。

    调研

    首先,看看WithValue到底发生了什么:

    package context
    
    func WithValue(parent Context, key, val interface{}) Context {
    	return &valueCtx{parent, key, val}
    }
    
    // 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{}
    }
    

    WithValue 通过把 key value 塞到了 valueCtx 的 struct 中,将数据保存下来。

    通过研究 context 包我们发现,不同的 功能的context有不同的实现

    package context
    
    // 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
    }
    
    // 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     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
    }
    

    但无一例外,全部是私有 struct ,同时是通过链表的形式将各个 context 串在一起的。

    这种情况对与我们想要做的事情是比较不好的,我们无法将 context 接口转换成实现来获取其内部保存的 key and value,而且由于有多种实现,我们无法得知下一个 context 是不是 valueCtx,需不需要跳过。

    思路

    既然这样,我们就只能用一些比较 hacker 的方法了:

    1. 定义一个自己的 valueCtx 内部数据结构与 context 包中一致
    2. 通过 unsafe.Pointer() 绕过类型检测,强制将 context.valueCtx 转换成我们的 valueCtx
    3. 获取内部的值保存在 map 中

    实践

    首先自定义一个我们自己的 valueCtx ,直接照搬 context 的实现就行:

    package main
    
    type valueCtx struct {
    	context.Context
    	key, val interface{}
    }
    

    然后强转并打印:

    package main
    
    func main() {
          ctx := context.Background()
          ctx = context.WithValue(ctx, "key1", "value1")
    
          valCtx := (*valueCtx)(unsafe.Pointer(&ctx))
          fmt.Printf("key: %v, value: %v", valCtx.key, valCtx.val)
          // panic: runtime error: invalid memory address or nil pointer dereference
    }
    

    事情并没有按我们预想的发生,这在编程的过程中再常见不过了,一定是有什么不对。在这种时候我们就应该去翻阅资料,去搞懂我们还模糊的部分,就会找到原因。不过,既然是文章,我就直接写我的结论了。

    这个要从接口类型的实现说起,golang 的接口的概念和实现是比较具体和复杂的,如果想进一步深入,请参阅这篇Stefno的文章,其非常深入和详细的讲解了 golang 中接口的方方面面。好了,回到我们的问题,现在对于 golang 的接口,我们只需要知道:在 golang 的接口里保存两个东西,其一是接口所持有的动态类型,其二是动态类型的值。

    结构如下:

    type iface struct {
    	itab, data uintptr
    }
    
    

    也就是说,当我们在转换接口的时候,其实是再把上面的结构强转为 valueCtx ,但其实我们期望的数据应该是保存在接口的动态类型值里,因此我们应该强转的是 iface.data。
    要做到这点,我们就需要将 context 先转换成 iface,再获取其中的 data:

    package main
    
    type iface struct {
    	itab, data uintptr
    }
    
    type valueCtx struct {
    	context.Context
    	key, val interface{}
    }
    
    func main() {
          ctx := context.Background()
          ctx = context.WithValue(ctx, "key1", "value1")
    
          ictx := (*iface)(unsafe.Pointer(&ctx))
          valCtx := (*valueCtx)(unsafe.Pointer(ictx.data))
          fmt.Printf("key: %v, value: %v", valCtx.key, valCtx.val)
          // output:
          // key: key1, value: value1
    }
    

    这回,我们终于获得其中的数据了。

    完善

    接下来,我们需要把 context 中的所有 key value 获取出来:

    package main
    
    func GetKeyValues(ctx context.Context) map[interface{}]interface{} {
          m := make(map[interface{}]interface{})
          getKeyValue(ctx, m)
          return m
    }
    
    func getKeyValue(ctx context.Context, m map[interface{}]interface{}) {
          ictx := *(*iface)(unsafe.Pointer(&ctx))
          if ictx.data == 0 {
                return
          }
    
          valCtx := (*valueCtx)(unsafe.Pointer(ictx.data))
          if valCtx != nil && valCtx.key != nil && valCtx.val != nil {
                m[valCtx.key] = valCtx.val
          }
          getKeyValue(valCtx.Context, m)
    }
    

    通过递归调用,我们可以很轻松的遍历所有 context,同时 golang 中的 nil 在底层其实就是一个int的0,这就是为什么我们把 nil 叫做 零值。所以,递归的退出条件也很简单,就是当 iface 中的 data 为0时,说明我们已经查找到 context 中的最后一个了。

    好了,以上就是所有的内容了,如果你想获取文章的具体实现和 demo 示例,可以到我的 github 上找到,谢谢你的阅读。