golang mapstructure庫的具體使用
簡介
mapstructure用于將通用的map[string]interface{}
解碼到對應的 Go 結構體中,或者執(zhí)行相反的操作。很多時候,解析來自多種源頭的數(shù)據(jù)流時,我們一般事先并不知道他們對應的具體類型。只有讀取到一些字段之后才能做出判斷。這時,我們可以先使用標準的encoding/json
庫將數(shù)據(jù)解碼為map[string]interface{}
類型,然后根據(jù)標識字段利用mapstructure
庫轉為相應的 Go 結構體以便使用。
快速使用
本文代碼采用 Go Modules。
首先創(chuàng)建目錄并初始化:
$ mkdir mapstructure && cd mapstructure $ go mod init github.com/darjun/go-daily-lib/mapstructure
下載mapstructure
庫:
$ 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
,他們的字段有些許不同?,F(xiàn)在,我們約定通信的 JSON 串中有一個type
字段。當type
的值為person
時,該 JSON 串表示的是Person
類型的數(shù)據(jù)。當type
的值為cat
時,該 JSON 串表示的是Cat
類型的數(shù)據(jù)。
上面代碼中,我們先用json.Unmarshal
將字節(jié)流解碼為map[string]interface{}
類型。然后讀取里面的type
字段。根據(jù)type
字段的值,再使用mapstructure.Decode
將該 JSON 串分別解碼為Person
和Cat
類型的值,并輸出。
實際上,Google Protobuf 通常也使用這種方式。在協(xié)議中添加消息 ID 或全限定消息名。接收方收到數(shù)據(jù)后,先讀取協(xié)議 ID 或全限定消息名。然后調用 Protobuf 的解碼方法將其解碼為對應的Message
結構。從這個角度來看,mapstructure
也可以用于網絡消息解碼,如果你不考慮性能的話?。
字段標簽
默認情況下,mapstructure
使用結構體中字段的名稱做這個映射,例如我們的結構體有一個Name
字段,mapstructure
解碼時會在map[string]interface{}
中查找鍵名name
。注意,這里的name
是大小寫不敏感的!
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
結構的數(shù)據(jù)要在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 中對應的值同時設置到這兩個字段中,即這兩個字段有相同的值。
未映射的值
如果源數(shù)據(jù)中有未映射的值(即結構體中無對應的字段),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) }
上面代碼中,我們?yōu)榻Y構體定義了一個Other
字段,用于保存未映射的鍵值。輸出結果:
other map[handsome:true height:1.8m]
逆向轉換
前面我們都是將map[string]interface{}
解碼到 Go 結構體中。mapstructure
當然也可以將 Go 結構體反向解碼為map[string]interface{}
。在反向解碼時,我們可以為某些字段設置mapstructure:",omitempty"
。這樣當這些字段為默認值時,就不會出現(xiàn)在結構的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)) }
上面代碼中,我們?yōu)?code>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
:在源數(shù)據(jù)中存在,但是目標結構中不存在的鍵名。
為了收集這些數(shù)據(jù),我們需要使用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
執(zhí)行轉換的過程中不可避免地會產生錯誤,例如 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
實現(xiàn)上面介紹的任何功能:
// mapstructure.go type DecoderConfig struct { ErrorUnused bool ZeroFields bool WeaklyTypedInput bool Metadata *Metadata Result interface{} TagName string }
各個字段含義如下:
ErrorUnused
:為true
時,如果輸入中的鍵值沒有與之對應的字段就返回錯誤;ZeroFields
:為true
時,在Decode
前清空目標map
。為false
時,則執(zhí)行的是map
的合并。用在struct
到map
的轉換中;WeaklyTypedInput
:實現(xiàn)WeakDecode/WeakDecodeMetadata
的功能;Metadata
:不為nil
時,收集Metadata
數(shù)據(jù);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
的方式實現(xiàn)了前面弱類型輸入小節(jié)中的示例代碼。實際上WeakDecode
內部就是通過這種方式實現(xiàn)的,下面是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
的對應字段,然后創(chuàng)建Decoder
對象,最后調用其Decode
方法實現(xiàn)的。
總結
mapstructure
實現(xiàn)優(yōu)雅,功能豐富,代碼結構清晰,非常推薦一看!
到此這篇關于golang mapstructure庫的具體使用的文章就介紹到這了,更多相關go mapstructure庫內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
linux中用shell快速安裝配置Go語言的開發(fā)環(huán)境
相信每位開發(fā)者都知道選擇一門開發(fā)語言,免不了需要安裝配置開發(fā)環(huán)境,所以這篇文章給大家分享了linux下使用shell一鍵安裝配置GO語言開發(fā)環(huán)境的方法,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-10-10Golang實現(xiàn)事務型內存數(shù)據(jù)庫的方法詳解
內存數(shù)據(jù)庫經我們經常用到,例如Redis,那么如何從零實現(xiàn)一個內存數(shù)據(jù)庫呢,本文旨在介紹如何使用Golang編寫一個KV內存數(shù)據(jù)庫MossDB2023-03-03