一道关于 json 和 slice 的题难倒了 80% 的人
在?Go語言愛好者周刊:第 65 期?刊首語發了一道題,以下代碼輸出什么?
package?main
import?(
?"encoding/json"
?"fmt"
)
type?AutoGenerated?struct?{
?Age???int????`json:"age"`
?Name??string?`json:"name"`
?Child?[]int??`json:"child"`
}
func?main()?{
?jsonStr1?:=?`{"age":?14,"name":?"potter",?"child":[1,2,3]}`
?a?:=?AutoGenerated{}
?json.Unmarshal([]byte(jsonStr1),?&a)
?aa?:=?a.Child
?fmt.Println(aa)
?jsonStr2?:=?`{"age":?12,"name":?"potter",?"child":[3,4,5,7,8,9]}`
?json.Unmarshal([]byte(jsonStr2),?&a)
?fmt.Println(aa)
}
?結果 80% 的人都答錯了。
?
結果為什么是 [1 2 3] [3 4 5] 呢?
這道題涉及到兩個知識點:
-
json 解析;
-
slice
1、json 解析
關于 json.Unmarshal 的文檔,不少人可能沒認真看。借此機會正好一起看下。https://docs.studygolang.com/pkg/encoding/json/#Unmarshal。
?
Unmarshal 解析 JSON 編碼的數據,并將結果存入 v 指向的值。如果 v 為 nil 或不是指針,則 Unmarshal 返回 InvalidUnmarshalError。
Unmarshal 和 Marshal 做相反的操作,必要時申請 map、slice 或指針,有如下的附加規則:
-
為了將 JSON 數據解碼寫入一個指針,Unmarshal 首先處理 JSON 數據為 JSON 字面值 null 的情況。此時,Unmarshal 會將指針設置為 nil。否則,Unmarshal 會將 JSON 數據解碼為指針所指向的值。如果指針為 nil,則 Unmarshal 為其分配一個新值并使指針指向它。
-
為了將 JSON 數據解碼為實現 Unmarshaler 接口的值,Unmarshal 調用該值的 UnmarshalJSON 方法,包括當輸入為 JSON ?null 時。否則,如果該值實現 encoding.TextUnmarshaler 且輸入是帶引號的 JSON 字符串,則 Unmarshal 會使用該字符串的未加引號形式來調用該值的 UnmarshalText 方法。
-
要將 json 數據解碼寫入一個結構體,函數會匹配輸入對象的鍵和 Marshal 使用的鍵(結構體字段名或者它的標簽指定的鍵名),優先選擇精確的匹配,但也接受大小寫不敏感的匹配;
-
為了將 JSON 數據解碼到結構中,Unmarshal 將傳入的對象鍵與 Marshal 使用的鍵(結構字段名稱或其 Tag)進行匹配,希望使用精確匹配,但還接受不區分大小寫的匹配。默認情況下,沒有相應結構字段的對象鍵將被忽略(有關替代方法,請參見 Decoder.DisallowUnknownFields)。
-
要將 JSON 數據解碼寫入一個接口類型值,Unmarshal 將其中之一存儲在接口值中:
Bool???????????????????對應JSON布爾類型 float64????????????????對應JSON數字類型 string?????????????????對應JSON字符串類型 []interface{}??????????對應JSON數組 map[string]interface{}?對應JSON對象 nil????????????????????對應JSON的null -
要將一個 JSON 數組解碼到切片(slice)中,Unmarshal 將切片長度重置為零,然后將每個元素 append 到切片中。特殊情況,如果將一個空的 JSON 數組解碼到一個切片中,Unmarshal 會用一個新的空切片替換該切片。
-
為了將 JSON 數組解碼為 Go 數組,Unmarshal 將 JSON 數組元素解碼為對應的 Go 數組元素。如果 Go 數組長度小于 JSON 數組,則其他 JSON 數組元素將被丟棄。如果 JSON 數組長度小于 Go 數組,則將其他 Go 數組元素會設置為零值。
-
要將 JSON 對象解碼到 map 中,Unmarshal 首先要建立將使用的 map。如果 map 為零,Unmarshal 會分配一個新 map。否則,Unmarshal 會重用現有 map,保留現有條目(item)。然后,Unmarshal 將來自 JSON 對象的鍵/值對存儲到 map 中。map 的鍵類型必須是任意字符串類型、整數或實現了 json.Unmarshaler 或 encoding.TextUnmarshaler 接口的類型。
-
如果 JSON 值不適用于給定的目標類型,或者 JSON 數字寫入目標類型時溢出,則 Unmarshal 會跳過該字段并盡最大可能完成解析。如果沒有遇到更多的嚴重錯誤,則 Unmarshal 返回一個 UnmarshalTypeError 來描述最早的此類錯誤。但無法確保有問題的字段之后的所有其余字段都將被解析到目標對象中。
-
JSON 的 null 值解碼為 Go 的接口、指針、切片時會將它們設為 nil,因為 null 在 JSON 里一般表示“不存在”。因此將 JSON null 解碼到任何其他 Go 類型中不會影響該值,并且不會產生任何錯誤。
-
解析帶引號的字符串時,無效的 UTF-8 或無效的 UTF-16 不會被視為錯誤。而是將它們替換為 Unicode 字符 U+FFFD。
?跟此題相關的是下面這點:
要將一個 JSON 數組解碼到切片(slice)中,Unmarshal 將切片長度重置為零,然后將每個元素 append 到切片中。特殊情況,如果將一個空的 JSON 數組解碼到一個切片中,Unmarshal 會用一個新的空切片替換該切片。
因此第一次解析時,a.Child 是 [1 2 3],aa 自然也是 [1 2 3]。第二次解析時,a.Child 的長度會被重置為 0,也就說里面的值會被重置(比如 a.Child = a.Child[:0]),然后將 3,4,5,7,8,9 一個個 append 到 a.Child 中。
而 append 操作可能會涉及到底層數組的擴容:當原來的容量不足時,會進行擴容。怎么擴容的呢?目前的版本(Go1.15.x)按照如下規則擴容:(擴容規則依賴具體實現,不同版本可能不一樣)
?//?Get?element?of?array,?growing?if?necessary.
if?v.Kind()?==?reflect.Slice?{
??//?Grow?slice?if?necessary
??if?i?>=?v.Cap()?{
????newcap?:=?v.Cap()?+?v.Cap()/2
????if?newcap?<?4?{
??????newcap?=?4
????}
????newv?:=?reflect.MakeSlice(v.Type(),?v.Len(),?newcap)
????reflect.Copy(newv,?v)
????v.Set(newv)
??}
??if?i?>=?v.Len()?{
????v.SetLen(i?+?1)
??}
}
-
初始容量最小為 4;
-
之后按照容量的一半擴容,所以容量是 4、6、9、13、19...
有人問上題為什么 aa 的容量是 4,這里正好解釋了。
因此,第一次解析,aa.Child 是:[1 2 3],cap = 4。第二次解析,aa.Child 先被重置,之后將 3,4,5,7,8,9 一個個 append,最后 aa.Child 是:[3 4 5 6 7 8 9], cap = 6。
2、slice
以上就是能從 json Unmarshal 文檔能學到的相關知識。接下來關鍵在于 slice。關于 slice 的知識,網上很多教程,這里只講解和該題相關的內容。
1)aa := a.Child 意味著什么?
先看 a.Child 的內部結構。
?
?賦值給 aa 后呢?aa 和 a.Child 共用底層數組。
?
這里有引入一個小知識點,aa := a.Child 后,以下代碼輸出的兩個地址是一樣的:
fmt.Printf("%p,%p\n",?a.Child,?aa)它們輸出的都是底層數組的地址,這里一定要注意。通過它們,你可以驗證底層數組擴容了(地址變了,表明擴容了)。
如果要輸出 slice 本身的地址,應該這樣:
fmt.Printf("%p,%p\n",?&a.Child,?&aa)?
2)執行第二次 json 解析后
根據上文的講解,底層數組從索引 0 位置開始依次被 3、4、5、7 填充。因為 aa 的 len 是 3,所以即使底層數組變成了 3、4、5、7,aa 看到卻是 3、4、5。
當再繼續解析時,底層數組容量不夠,因此進行擴容,cap 變成 6,將原底層數組的元素拷貝一份到新的數組中。所以最后 a.Child 的底層數組是這個新的底層數組:[3 4 5 7 8 9],cap = 6。而 aa 的底層數組還是原來的。最后的內部表示是這樣的。
?
?
3、小結
總結起來兩點:
-
json 解析的規則,文檔上明確說明了;(有人評論說跟 json 沒關系,但我覺得很多人根本不知道 json 對這塊是怎么處理的,正因為有這樣的處理,才引出了 slice 擴容的問題)
-
slice 內部表示和 append 導致擴容。
總結
以上是生活随笔為你收集整理的一道关于 json 和 slice 的题难倒了 80% 的人的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kafka集群管理工具kafka-man
- 下一篇: 工作中常用的kafka命令