Redigo issue 487
How to scan struct with nested fields?#487
為了更好的理解本篇文章,建議先閱讀issue原文
一、問題是什麼
將HGETALL
命令返回的資料,解析到對應的結構體UserInfo
中,但是結構體中的*LiteUser
欄位的資料未能成功解析。
如果將 *LiteUser
改為 LiteUser
就可以了。
二、復現問題
- copy issue 中的程式碼,go module 安裝 redigo,再準備一臺redis服務。
- go.mod 中的 redigo中的版本設定為issue未修改前的版本:v1.8.1。
- 執行程式碼,會復現此issue的問題,
*LiteUser
欄位的資料未能成功解析。
試試最新版的程式碼,執行下來的情況:
- go.mod 中的 redigo 中的版本設定為issue最新版本:v1.8.8。
- 執行程式碼,問題沒有出現。
注意,為了在最新版本下復現問題,需在示例程式碼大約 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
方法得到可以被設定的變數。
4.2 newUser.LiteUser = &LiteUser{}
int、string等為值型別的,即使不進行初始化,只有宣告,值也會預設成這個型別的“零”值。
但是像 Map、Slice、Channel等引用變數,需要在使用前先make()
的。
同理&
型別的變數,他的值是儲存的是記憶體地址,那就必須先要初始化一個LiteUser
的結構體,然後將他的記憶體地址,賦值給newUser.LiteUser
,才能正常使用。