mapstructure
用於將通用的map[string]interface{}
解碼到對應的 Go 結構體中,或者執行相反的操做。不少時候,解析來自多種源頭的數據流時,咱們通常事先並不知道他們對應的具體類型。只有讀取到一些字段以後才能作出判斷。這時,咱們能夠先使用標準的encoding/json
庫將數據解碼爲map[string]interface{}
類型,而後根據標識字段利用mapstructure
庫轉爲相應的 Go 結構體以便使用。git
本文代碼採用 Go Modules。github
首先建立目錄並初始化:golang
$ mkdir mapstructure && cd mapstructure $ go mod init github.com/darjun/go-daily-lib/mapstructure
下載mapstructure
庫:json
$ go get github.com/mitchellh/mapstructure
使用:微信
package main import ( "encoding/json" "fmt" "log" "github.com/mitchellh/mapstructure" ) type Person struct { Name string Age int Job string } type Cat struct { Name string Age int Breed string } func main() { datas := []string{` { "type": "person", "name":"dj", "age":18, "job": "programmer" } `, ` { "type": "cat", "name": "kitty", "age": 1, "breed": "Ragdoll" } `, } for _, data := range datas { var m map[string]interface{} err := json.Unmarshal([]byte(data), &m) if err != nil { log.Fatal(err) } switch m["type"].(string) { case "person": var p Person mapstructure.Decode(m, &p) fmt.Println("person", p) case "cat": var cat Cat mapstructure.Decode(m, &cat) fmt.Println("cat", cat) } } }
運行結果:網絡
$ go run main.go person {dj 18 programmer} cat {kitty 1 Ragdoll}
咱們定義了兩個結構體Person
和Cat
,他們的字段有些許不一樣。如今,咱們約定通訊的 JSON 串中有一個type
字段。當type
的值爲person
時,該 JSON 串表示的是Person
類型的數據。當type
的值爲cat
時,該 JSON 串表示的是Cat
類型的數據。性能
上面代碼中,咱們先用json.Unmarshal
將字節流解碼爲map[string]interface{}
類型。而後讀取裏面的type
字段。根據type
字段的值,再使用mapstructure.Decode
將該 JSON 串分別解碼爲Person
和Cat
類型的值,並輸出。學習
實際上,Google Protobuf 一般也使用這種方式。在協議中添加消息 ID 或全限定消息名。接收方收到數據後,先讀取協議 ID 或全限定消息名。而後調用 Protobuf 的解碼方法將其解碼爲對應的Message
結構。從這個角度來看,mapstructure
也能夠用於網絡消息解碼,若是你不考慮性能的話😄。spa
默認狀況下,mapstructure
使用結構體中字段的名稱作這個映射,例如咱們的結構體有一個Name
字段,mapstructure
解碼時會在map[string]interface{}
中查找鍵名name
。注意,這裏的name
是大小寫不敏感的!code
type Person struct { Name string }
固然,咱們也能夠指定映射的字段名。爲了作到這一點,咱們須要爲字段設置mapstructure
標籤。例以下面使用username
代替上例中的name
:
type Person struct { Name string `mapstructure:"username"` }
看示例:
type Person struct { Name string `mapstructure:"username"` Age int Job string } type Cat struct { Name string Age int Breed string } func main() { datas := []string{` { "type": "person", "username":"dj", "age":18, "job": "programmer" } `, ` { "type": "cat", "name": "kitty", "Age": 1, "breed": "Ragdoll" } `, ` { "type": "cat", "Name": "rooooose", "age": 2, "breed": "shorthair" } `, } for _, data := range datas { var m map[string]interface{} err := json.Unmarshal([]byte(data), &m) if err != nil { log.Fatal(err) } switch m["type"].(string) { case "person": var p Person mapstructure.Decode(m, &p) fmt.Println("person", p) case "cat": var cat Cat mapstructure.Decode(m, &cat) fmt.Println("cat", cat) } } }
上面代碼中,咱們使用標籤mapstructure:"username"
將Person
的Name
字段映射爲username
,在 JSON 串中咱們須要設置username
才能正確解析。另外,注意到,咱們將第二個 JSON 串中的Age
和第三個 JSON 串中的Name
首字母大寫了,可是並無影響解碼結果。mapstructure
處理字段映射是大小寫不敏感的。
結構體能夠任意嵌套,嵌套的結構被認爲是擁有該結構體名字的另外一個字段。例如,下面兩種Friend
的定義方式對於mapstructure
是同樣的:
type Person struct { Name string } // 方式一 type Friend struct { Person } // 方式二 type Friend struct { Person Person }
爲了正確解碼,Person
結構的數據要在person
鍵下:
map[string]interface{} { "person": map[string]interface{}{"name": "dj"}, }
咱們也能夠設置mapstructure:",squash"
將該結構體的字段提到父結構中:
type Friend struct { Person `mapstructure:",squash"` }
這樣只須要這樣的 JSON 串,無效嵌套person
鍵:
map[string]interface{}{ "name": "dj", }
看示例:
type Person struct { Name string } type Friend1 struct { Person } type Friend2 struct { Person `mapstructure:",squash"` } func main() { datas := []string{` { "type": "friend1", "person": { "name":"dj" } } `, ` { "type": "friend2", "name": "dj2" } `, } for _, data := range datas { var m map[string]interface{} err := json.Unmarshal([]byte(data), &m) if err != nil { log.Fatal(err) } switch m["type"].(string) { case "friend1": var f1 Friend1 mapstructure.Decode(m, &f1) fmt.Println("friend1", f1) case "friend2": var f2 Friend2 mapstructure.Decode(m, &f2) fmt.Println("friend2", f2) } } }
注意對比Friend1
和Friend2
使用的 JSON 串的不一樣。
另外須要注意一點,若是父結構體中有同名的字段,那麼mapstructure
會將JSON 中對應的值同時設置到這兩個字段中,即這兩個字段有相同的值。
若是源數據中有未映射的值(即結構體中無對應的字段),mapstructure
默認會忽略它。
咱們能夠在結構體中定義一個字段,爲其設置mapstructure:",remain"
標籤。這樣未映射的值就會添加到這個字段中。注意,這個字段的類型只能爲map[string]interface{}
或map[interface{}]interface{}
。
看示例:
type Person struct { Name string Age int Job string Other map[string]interface{} `mapstructure:",remain"` } func main() { data := ` { "name": "dj", "age":18, "job":"programmer", "height":"1.8m", "handsome": true } ` var m map[string]interface{} err := json.Unmarshal([]byte(data), &m) if err != nil { log.Fatal(err) } var p Person mapstructure.Decode(m, &p) fmt.Println("other", p.Other) }
上面代碼中,咱們爲結構體定義了一個Other
字段,用於保存未映射的鍵值。輸出結果:
other map[handsome:true height:1.8m]
前面咱們都是將map[string]interface{}
解碼到 Go 結構體中。mapstructure
固然也能夠將 Go 結構體反向解碼爲map[string]interface{}
。在反向解碼時,咱們能夠爲某些字段設置mapstructure:",omitempty"
。這樣當這些字段爲默認值時,就不會出如今結構的map[string]interface{}
中:
type Person struct { Name string Age int Job string `mapstructure:",omitempty"` } func main() { p := &Person{ Name: "dj", Age: 18, } var m map[string]interface{} mapstructure.Decode(p, &m) data, _ := json.Marshal(m) fmt.Println(string(data)) }
上面代碼中,咱們爲Job
字段設置了mapstructure:",omitempty"
,且對象p
的Job
字段未設置。運行結果:
$ go run main.go {"Age":18,"Name":"dj"}
Metadata
解碼時會產生一些有用的信息,mapstructure
可使用Metadata
收集這些信息。Metadata
結構以下:
// mapstructure.go type Metadata struct { Keys []string Unused []string }
Metadata
只有兩個導出字段:
Keys
:解碼成功的鍵名;Unused
:在源數據中存在,可是目標結構中不存在的鍵名。爲了收集這些數據,咱們須要使用DecodeMetadata
來代替Decode
方法:
type Person struct { Name string Age int } func main() { m := map[string]interface{}{ "name": "dj", "age": 18, "job": "programmer", } var p Person var metadata mapstructure.Metadata mapstructure.DecodeMetadata(m, &p, &metadata) fmt.Printf("keys:%#v unused:%#v\n", metadata.Keys, metadata.Unused) }
先定義一個Metadata
結構,傳入DecodeMetadata
收集解碼的信息。運行結果:
$ go run main.go keys:[]string{"Name", "Age"} unused:[]string{"job"}
mapstructure
執行轉換的過程當中不可避免地會產生錯誤,例如 JSON 中某個鍵的類型與對應 Go 結構體中的字段類型不一致。Decode/DecodeMetadata
會返回這些錯誤:
type Person struct { Name string Age int Emails []string } func main() { m := map[string]interface{}{ "name": 123, "age": "bad value", "emails": []int{1, 2, 3}, } var p Person err := mapstructure.Decode(m, &p) if err != nil { fmt.Println(err.Error()) } }
上面代碼中,結構體中Person
中字段Name
爲string
類型,但輸入中name
爲int
類型;字段Age
爲int
類型,但輸入中age
爲string
類型;字段Emails
爲[]string
類型,但輸入中emails
爲[]int
類型。故Decode
返回錯誤。運行結果:
$ go run main.go 5 error(s) decoding: * 'Age' expected type 'int', got unconvertible type 'string' * 'Emails[0]' expected type 'string', got unconvertible type 'int' * 'Emails[1]' expected type 'string', got unconvertible type 'int' * 'Emails[2]' expected type 'string', got unconvertible type 'int' * 'Name' expected type 'string', got unconvertible type 'int'
從錯誤信息中很容易看出哪裏出錯了。
有時候,咱們並不想對結構體字段類型和map[string]interface{}
的對應鍵值作強類型一致的校驗。這時可使用WeakDecode/WeakDecodeMetadata
方法,它們會嘗試作類型轉換:
type Person struct { Name string Age int Emails []string } func main() { m := map[string]interface{}{ "name": 123, "age": "18", "emails": []int{1, 2, 3}, } var p Person err := mapstructure.WeakDecode(m, &p) if err == nil { fmt.Println("person:", p) } else { fmt.Println(err.Error()) } }
雖然鍵name
對應的值123
是int
類型,可是在WeakDecode
中會將其轉換爲string
類型以匹配Person.Name
字段的類型。一樣的,age
的值"18"
是string
類型,在WeakDecode
中會將其轉換爲int
類型以匹配Person.Age
字段的類型。
須要注意一點,若是類型轉換失敗了,WeakDecode
一樣會返回錯誤。例如將上例中的age
設置爲"bad value"
,它就不能轉爲int
類型,故而返回錯誤。
除了上面介紹的方法外,mapstructure
還提供了更靈活的解碼器(Decoder
)。能夠經過配置DecoderConfig
實現上面介紹的任何功能:
// mapstructure.go type DecoderConfig struct { ErrorUnused bool ZeroFields bool WeaklyTypedInput bool Metadata *Metadata Result interface{} TagName string }
各個字段含義以下:
ErrorUnused
:爲true
時,若是輸入中的鍵值沒有與之對應的字段就返回錯誤;ZeroFields
:爲true
時,在Decode
前清空目標map
。爲false
時,則執行的是map
的合併。用在struct
到map
的轉換中;WeaklyTypedInput
:實現WeakDecode/WeakDecodeMetadata
的功能;Metadata
:不爲nil
時,收集Metadata
數據;Result
:爲結果對象,在map
到struct
的轉換中,Result
爲struct
類型。在struct
到map
的轉換中,Result
爲map
類型;TagName
:默認使用mapstructure
做爲結構體的標籤名,能夠經過該字段設置。看示例:
type Person struct { Name string Age int } func main() { m := map[string]interface{}{ "name": 123, "age": "18", "job": "programmer", } var p Person var metadata mapstructure.Metadata decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ WeaklyTypedInput: true, Result: &p, Metadata: &metadata, }) if err != nil { log.Fatal(err) } err = decoder.Decode(m) if err == nil { fmt.Println("person:", p) fmt.Printf("keys:%#v, unused:%#v\n", metadata.Keys, metadata.Unused) } else { fmt.Println(err.Error()) } }
這裏用Decoder
的方式實現了前面弱類型輸入小節中的示例代碼。實際上WeakDecode
內部就是經過這種方式實現的,下面是WeakDecode
的源碼:
// mapstructure.go func WeakDecode(input, output interface{}) error { config := &DecoderConfig{ Metadata: nil, Result: output, WeaklyTypedInput: true, } decoder, err := NewDecoder(config) if err != nil { return err } return decoder.Decode(input) }
再實際上,Decode/DecodeMetadata/WeakDecodeMetadata
內部都是先設置DecoderConfig
的對應字段,而後建立Decoder
對象,最後調用其Decode
方法實現的。
mapstructure
實現優雅,功能豐富,代碼結構清晰,很是推薦一看!
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~