一種獲取context中keys和values的高效方法 | golang

我們知道,在 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 上找到,謝謝你的閱讀。