匿名成員
結構體中嵌套結構體
Go 切片
Go 數組
Go map
結構體中的匿名成員
我們回來看一下上一篇文章中的 marshalToValues 函數,其中有一行 “ft.Anonymous”:
func marshalToValues(in interface{}) (kv url.Values, err error) {
// ......
// 迭代每一個字段
for i := 0; i < numField; i++ {
fv := v.Field(i) // field value
ft := t.Field(i) // field type
if ft.Anonymous {
// TODO: 後文再處理
continue
}
// ......
}
return kv, nil
}
前文提過,這表示當前的字段是一個匿名字段。在 Go 中,匿名成員經常用於實現接近於繼承的功能,比如:
type Dog struct{
Name string
}
func (d *Dog) Woof() {
// ......
}
type Husky struct{
Dog
}
這樣一來,類型 Husky 就 “繼承” 了 Dog 類型的 Name 字段,以及 Woof() 函數。
但是需要注意的是,在 Go 中,這不是真正意義上的繼承。我們在通過 reflect 解析 Husky 的結構時會發現,它包含了一個 Dog 類型結構體,而這個結構體在代碼分支中,就會進入到前文的 if ft.Anonymous {} 分支中。
第二個需要注意的點是:在 Go 中,不僅僅是 struct 能夠作為匿名成員,實際上任意類型都可以匿名。因此在代碼中需要區分這種情況。
OK,知道了上述注意點之後,我們就可以來處理匿名結構體的情況啦。如果説匿名結構體的主要目的是為了繼承的效果,那麼我們對待匿名結構體中的成員的態度,就是當作對待結構體本身普通成員的態度一樣。把我們已經實現了的 marshalToValues 的邏輯稍微調整一下,將迭代邏輯單獨抽出來,方便遞歸就行——注意下文 readFieldToKV 函數的第一個條件判斷代碼塊:
func marshalToValues(in interface{}) (kv url.Values, err error) {
// ......
// 迭代每一個字段
for i := 0; i < numField; i++ {
fv := v.Field(i) // field value
ft := t.Field(i) // field type
readFieldToKV(&fv, &ft, kv) // 主要邏輯抽出到函數中進行處理
}
return kv, nil
}
func readFieldToKV(fv reflect.Value, ft reflect.StructField, kv url.Values) {
if ft.Anonymous {
numField := fv.NumField()
for i := 0; i < numField; i++ {
ffv := fv.Field(i)
fft := ft.Type.Field(i)
readFieldToKV(&ffv, &fft, kv)
}
return
}
if !fv.CanInterface() {
return
}
// ...... 原來的 for 循環中的主邏輯
}
結構體中的切片和數組
上一小節我們對 marshalToValues 的邏輯進行了調整,將 readFieldToKV 函數抽了出來。這個函數首先判斷 if ft.Anonymous,也就是是否匿名;然後再判斷 if !fv.CanInterface(),也就是是否可以導出。
再往下走,我們處理的是結構體中的每一個成員。上一篇文章中我們已經處理了所有的簡單數據類型,但是還有不少承載有效數據的變量類型我們還沒有處理。這一小節,我們來看看切片和數組要如何做。
首先在本文中我們規定,對於數組,只支持成員為基本類型(bool,數字、字符串、布爾值)的數組,而不支持所謂 “任意類型”(也就是 interface{})和結構體(struct)的數組。
究其原因,是因為後我們我們準備使用點分隔符來區分數組內的數組,也就是説,採用諸如 msg.data 來表示 msg 結構體中的 data 成員。而 URL query 是採用同一個 key 重複出現多次來實現數組類型的,那如果重複出現了 msg.data,那我們應該解釋為 msg[n].data 呢,還是 msg.data[n] 呢?
為了實現這一段代碼,我們修改前文的 readFieldToKV 為:
func readFieldToKV(fv reflect.Value, ft reflect.StructField, kv url.Values) {
if ft.Anonymous {
numField := fv.NumField()
for i := 0; i < numField; i++ {
ffv := fv.Field(i)
fft := ft.Type.Field(i)
readFieldToKV(&ffv, &fft, kv)
}
return
}
if !fv.CanInterface() {
return
}
tg := readTag(ft, "url")
if tg.Name() == "-" {
return
}
// 將寫 KV 的功能獨立成一個函數
readFieldValToKV(fv, tg, kv)
}
然後我們看看該函數中調用的子函數 readFieldValToKV 的內容,這個函數大概50行,我們分成幾塊來看:
func readFieldValToKV(v *reflect.Value, tg tags, kv url.Values) {
key := tg.Name()
val := ""
var vals []string
omitempty := tg.Has("omitempty")
isSliceOrArray := false
switch v.Type().Kind() {
// ......
// 代碼塊 1
// ......
// ......
// 代碼塊 2
// ......
}
// 數組使用 Add 函數
if isSliceOrArray {
for _, v := range vals {
kv.Add(key, v)
}
return
}
if val == "" && omitempty {
return
}
kv.Set(key, val)
}
其中 代碼塊1 的內容是將基本類型的數據轉為 val string 類型變量值。這沒什麼好説的,前兩篇文章已經解釋過了
而 代碼塊2 則是對切片和數組的解析,內容如下:
case reflect.Slice, reflect.Array:
isSliceOrArray = true
elemTy := v.Type().Elem()
switch elemTy.Kind() {
default:
// 什麼也不做,omitempty 對數組而言沒有意義
case reflect.String:
vals = readStringArray(v)
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
vals = readIntArray(v)
case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
vals = readUintArray(v)
case reflect.Bool:
vals = readBoolArray(v)
case reflect.Float64, reflect.Float32:
vals = readFloatArray(v)
}
我們取其中的 readStringArray 為例:
func readStringArray(v *reflect.Value) (vals []string) {
count := v.Len() // Len() 函數
for i := 0; i < count; i++ {
child := v.Index(i) // Index() 函數
s := child.String()
vals = append(vals, s)
}
return
}
一目瞭然。這裏涉及了 reflect.Value 的兩個函數:
Len(): 對於切片、數組,甚至是 map,這個函數返回其成員的數量
Index(int): 對於切片、數組,這個函數都返回了其成員的位置
後面的操作,就跟標準數字字段一樣了,讀取 reflect.Value 中的值並返回。
到這裏為止的代碼,對應 Github 上的 40d0693 版本。讀者可以查看 diff 瞭解相比上一篇文章,為了支持匿名成員和切片/數字類型,我們做了哪些代碼改動。
結構體中的結構體
前文已經簡單提過了:我們打算用類似點操作符的模式,來處理結構體中的非匿名、可導出的結構體。如果對於 JSON,這種就相當於 “對象中的對象”。
從技術角度,所需的知識其實在前面都已經有了,我們在這一小節中為了支持結構體中的結構體這樣的功能,我們需要對源文件做進一步的調整,主要注意的功能點有以下這些:
給相關的函數添加 prefix 參數,支持遞歸調用以實現多層嵌套
結構體中的結構體的常見模式,包括結構體,以及結構體指針兩種情況,需要分別處理
添加 struct in struct 功能的代碼版本,則是緊跟着上一版本 070cb3b,讀者可以查看 diff 差異,可以看到我的改動其實不多,基本上也就對應着上述兩項,短短十來行就實現了對 struct in struct 的支持。
Go map
這是複雜數據類型的最後一個。這裏我們説明一下如何從 reflect.Value 中判斷對象是否為 map,以及如何從 map 類型的 reflect.Value 中獲取 key 和 value 值。
首先我們梳理一下,如果遇到 map 類型的話,我們的判斷邏輯:
首先判斷 map 的 key 類型,我們只支持 key 為 string 的 map
然後判斷 map 的 value 類型:
如果是基本數據類型自不必説,支持——比如 map[string]string,map[string]int 之類的
如果是 struct,也支持,就當作 struct in struct 處理即可
如果是 slice 或者是 array,也按照本文第二小節的處理模式來處理
如果是 interface{} 類型,那麼就需要一個個判斷每一個值的類型是否支持了
OK,這裏我們先介紹 reflect.Value 在處理 map 時所需要使用的幾個函數。在能夠確定當前 reflect.Value 的 kind 等於 reflect.Map 的前提下:
判斷 key 的類型是否為 string:if v.Type().Key().Kind() != reflect.String {return},也就是 reflect.Type 的 Key() 函數,可以獲得 key 的類型。
獲得 value 的類型,使用:v.Type().Elem(),返回一個新的 reflect.Type 值,這代表了 map 的 value 的類型。
獲得 map 中的所有 key 值,使用:v.MapKeys(),返回一個 []reflect.Value 類型
根據 key 獲得 map 中的 value 值:v.MapIndex(k),入參
此外,如果要迭代 map 中的 kv,還可以使用 MapRange 函數,讀者可以查閲 godoc
需要添加的代碼也不多,在前文 readFieldValToKV 的 “代碼塊2” 後面再添加一個 “代碼塊3” 就行,大致如下:
case reflect.Map:
if v.Type().Key().Kind() != reflect.String {
return // 不支持,直接跳過
}
keys := v.MapKeys()
for _, k := range keys {
subV := v.MapIndex(k)
if subV.Kind() == reflect.Interface {
subV = reflect.ValueOf(subV.Interface())
}
readFieldValToKV(&subV, tags{k.String()}, kv, key)
}
return
為什麼要加一句 if subV.Kind() == reflect.Interface 的條件塊呢,主要是針對 map[string]interface{} 的支持,因為這種 map 的 value 類型是 reflect.Interface,如果要拿到其底層數據類型的值得,需要再加一句 subV = reflect.ValueOf(subV.Interface()),這樣 reflect.Value 的 Kind 才會是其真正的類型。
尾聲
到這裏為止的代碼則對應 a18ab4a 版本。至此,通過 reflect 解析結構體的內容就算説明完了。
我們只講了 marshal 的內容,至於 unmarshal 的過程,在解析參數類型和結構的角度是差不多的,不同的也就只有如何給 interface{} 參數賦值了。筆者爭取下一篇文章就寫一下這相關的內容吧。
相關文章推薦
GO編程模式:切片,接口,時間和性能
還在用 map「string」interface{} 處理 JSON?告訴你一個更高效的方法——jsonvalue
Go 語言原生的 json 包有什麼問題?如何更好地處理 JSON 數據?
手把手教你用 reflect 包解析 Go 的結構體 - Step 1: 參數類型檢查
手把手教你用 reflect 包解析 Go 的結構體 - Step 2: 結構體成員遍歷