Redigo: ScanStruct()匿名指標欄位的解析

sown發表於2022-04-05

Redigo issue 487

How to scan struct with nested fields?#487
為了更好的理解本篇文章,建議先閱讀issue原文

一、問題是什麼

HGETALL 命令返回的資料,解析到對應的結構體UserInfo中,但是結構體中的*LiteUser欄位的資料未能成功解析。

如果將 *LiteUser 改為 LiteUser 就可以了。

二、復現問題

  1. copy issue 中的程式碼,go module 安裝 redigo,再準備一臺redis服務。
  2. go.mod 中的 redigo中的版本設定為issue未修改前的版本:v1.8.1。
  3. 執行程式碼,會復現此issue的問題,*LiteUser欄位的資料未能成功解析。

試試最新版的程式碼,執行下來的情況:

  1. go.mod 中的 redigo 中的版本設定為issue最新版本:v1.8.8。
  2. 執行程式碼,問題沒有出現。
注意,為了在最新版本下復現問題,需在示例程式碼大約 73行下面,加入如下程式碼(後面再回過頭來看看這個問題):
...
var newUser UserInfo
newUser.LiteUser = &LiteUser{}
...

三、怎麼解決的

具體內容詳見 pr 490

在看如何解決之前,先梳理一下執行流程:

3.1 解析資料到結構體變數

當執行HGETALL從 Redis 中拿到了資料後,需要將資料解析到結構體的成員變數上,就像從 MySQL 拿出來資料,解析到結構體成員變數上是一個意思。

Redigo 提供好了一個方法,將資料和結構體變數傳進去,資料就會解析到newUser結構體上:

redis.ScanStruct(v, &newUser)

3.2 ScanStruct

接下來,看下redis.ScanStruct()都做了些什麼。
我梳理總結了一下過程中呼叫的方法:

// 將資料解析到structSpecForType返回的結構體成員上
func ScanStruct(src []interface{}, dest interface{}) error {
    //獲取變數指標
    d := reflect.ValueOf(dest)
    //獲取指標指向的變數
    d = d.Elem()
    structSpecForType(d.Type())
    ...
}

// 根據傳入的reflect.Type,先去快取中查詢是否解析過,如果沒有呼叫compileStructSpec
func structSpecForType(t reflect.Type) *structSpec {
    ...
    compileStructSpec(t, make(map[string]int), nil, ss)
    ...
}

3.3 compileStructSpec

compileStructSpec方法實現的就是型別解析,問題其實就出在了這。

先將梳理過的總結貼出來:

  • 使用反射將資料解析到 &newUser 結構體 的所有成員變數
  • 在V1.8.1版本及以前,只解析了 reflect.Struct(LiteUser),未處理 reflect.Ptr(*LiteUser)
  • 在V1.8.2 版本及以後,增加了 reflect.Ptr 的判斷

下面是核心邏輯
修復前:

func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
   // t.NumField()獲取結構體型別的所有欄位的個數
   for i := 0; i < t.NumField(); i++ {
      // t.Field()返回指定的欄位,型別為 StructField
      f := t.Field(i)
      switch {
      // f.PkgPath 包路徑不為空 且 不是匿名函式
      // f.Anonymous 表示該欄位是否為匿名欄位
      case f.PkgPath != "" && !f.Anonymous:
         // 忽略未匯出的:結構體中的某個成員改為小寫(私有),就會進到這個case
         // Ignore unexported fields.
      // UserInfo中的成員LiteUser,並未設定 name,為匿名欄位,就會進到這個case
      case f.Anonymous:
         // f.Type.Kind() 獲取種類
        // 如果當前type為結構體,進行遞迴呼叫,以處理當前type內所有結構體成員
        // 對於 `LiteUser` 會進到這個 case
         if f.Type.Kind() == reflect.Struct {
            compileStructSpec(f.Type, depth, append(index, i), ss)
         }

修復後:

...
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
LOOP:
   for i := 0; i < t.NumField(); i++ {
      f := t.Field(i)
      switch {
      case f.PkgPath != "" && !f.Anonymous:
         // Ignore unexported fields.
      case f.Anonymous:
         switch f.Type.Kind() {
         case reflect.Struct:
            compileStructSpec(f.Type, depth, append(index, i), ss)
         // 這裡是變動的部分,對於 `*LiteUser` 會進到這個 case
         case reflect.Ptr:
            // 如果當前欄位的type的值為結構體,進行遞迴呼叫,以處理當前欄位內所有結構體成員
            // f.Type.Kind()返回的是前f的種類,也就是reflect.Ptr
            // f.Type.Elem().Kind() 返回的是前f的值的種類,也就是reflect.Struct
            // TODO(steve): Protect against infinite recursion.
            if f.Type.Elem().Kind() == reflect.Struct {
               compileStructSpec(f.Type.Elem(), depth, append(index, i), ss)
            }
         }
...

OK~,問題解決!

四、擴充套件

4.1 反射

compileStructSpec方法內部,主要就是通過反射來實現的。
這裡重點要說下,為啥d := reflect.ValueOf(dest)完了之後,還要用d = d.Elem() ,引用《Go 語言設計與實現》的一句話

由於 Go 語言的函式呼叫都是值傳遞的,所以我們只能只能用迂迴的方式改變原變數:先獲取指標對應的reflect.Value,再通過reflect.Value.Elem方法得到可以被設定的變數。

參考
Go 語言涉及與實現-反射

4.2 newUser.LiteUser = &LiteUser{}

int、string等為值型別的,即使不進行初始化,只有宣告,值也會預設成這個型別的“零”值。
但是像 Map、Slice、Channel等引用變數,需要在使用前先make()的。

同理&型別的變數,他的值是儲存的是記憶體地址,那就必須先要初始化一個LiteUser的結構體,然後將他的記憶體地址,賦值給newUser.LiteUser,才能正常使用。

image.png

相關文章