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

[CPP] STL 简介

我们知道,在 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 的实现就行:

核酸检测:让我明白AQS原理

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 上找到,谢谢你的阅读。

RecyclerView 源码分析(二) —— 缓存机制,RecyclerView 源码分析(一) —— 绘制流程解析,RecyclerView 源码分析(一) —— 绘制流程解析

相关推荐

发表评论

路人甲

网友评论(0)