Go 每日一庫之 mapstructure
簡介
mapstructure用于將通用的map[string]interface{}解碼到對應(yīng)的 Go 結(jié)構(gòu)體中,或者執(zhí)行相反的操作。很多時候,解析來自多種源頭的數(shù)據(jù)流時,我們一般事先并不知道他們對應(yīng)的具體類型。只有讀取到一些字段之后才能做出判斷。這時,我們可以先使用標(biāo)準(zhǔn)的encoding/json庫將數(shù)據(jù)解碼為map[string]interface{}類型,然后根據(jù)標(biāo)識字段利用mapstructure庫轉(zhuǎn)為相應(yīng)的 Go 結(jié)構(gòu)體以便使用。
快速使用
本文代碼采用 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)
????}
??}
}
運行結(jié)果:
$ go run main.go
person {dj 18 programmer}
cat {kitty 1 Ragdoll}
我們定義了兩個結(jié)構(gòu)體Person和Cat,他們的字段有些許不同?,F(xiàn)在,我們約定通信的 JSON 串中有一個type字段。當(dāng)type的值為person時,該 JSON 串表示的是Person類型的數(shù)據(jù)。當(dāng)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 或全限定消息名。然后調(diào)用 Protobuf 的解碼方法將其解碼為對應(yīng)的Message結(jié)構(gòu)。從這個角度來看,mapstructure也可以用于網(wǎng)絡(luò)消息解碼,如果你不考慮性能的話?。
字段標(biāo)簽
默認(rèn)情況下,mapstructure使用結(jié)構(gòu)體中字段的名稱做這個映射,例如我們的結(jié)構(gòu)體有一個Name字段,mapstructure解碼時會在map[string]interface{}中查找鍵名name。注意,這里的name是大小寫不敏感的!
type?Person?struct?{
??Name?string
}
當(dāng)然,我們也可以指定映射的字段名。為了做到這一點,我們需要為字段設(shè)置mapstructure標(biāo)簽。例如下面使用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)
????}
??}
}
上面代碼中,我們使用標(biāo)簽mapstructure:"username"將Person的Name字段映射為username,在 JSON 串中我們需要設(shè)置username才能正確解析。另外,注意到,我們將第二個 JSON 串中的Age和第三個 JSON 串中的Name首字母大寫了,但是并沒有影響解碼結(jié)果。mapstructure處理字段映射是大小寫不敏感的。
內(nèi)嵌結(jié)構(gòu)
結(jié)構(gòu)體可以任意嵌套,嵌套的結(jié)構(gòu)被認(rèn)為是擁有該結(jié)構(gòu)體名字的另一個字段。例如,下面兩種Friend的定義方式對于mapstructure是一樣的:
type?Person?struct?{
??Name?string
}
//?方式一
type?Friend?struct?{
??Person
}
//?方式二
type?Friend?struct?{
??Person?Person
}
為了正確解碼,Person結(jié)構(gòu)的數(shù)據(jù)要在person鍵下:
map[string]interface{}?{
??"person":?map[string]interface{}{"name":?"dj"},
}
我們也可以設(shè)置mapstructure:",squash"將該結(jié)構(gòu)體的字段提到父結(jié)構(gòu)中:
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 串的不同。
另外需要注意一點,如果父結(jié)構(gòu)體中有同名的字段,那么mapstructure會將JSON 中對應(yīng)的值同時設(shè)置到這兩個字段中,即這兩個字段有相同的值。
未映射的值
如果源數(shù)據(jù)中有未映射的值(即結(jié)構(gòu)體中無對應(yīng)的字段),mapstructure默認(rèn)會忽略它。
我們可以在結(jié)構(gòu)體中定義一個字段,為其設(shè)置mapstructure:",remain"標(biāo)簽。這樣未映射的值就會添加到這個字段中。注意,這個字段的類型只能為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(jié)構(gòu)體定義了一個Other字段,用于保存未映射的鍵值。輸出結(jié)果:
other?map[handsome:true?height:1.8m]
逆向轉(zhuǎn)換
前面我們都是將map[string]interface{}解碼到 Go 結(jié)構(gòu)體中。mapstructure當(dāng)然也可以將 Go 結(jié)構(gòu)體反向解碼為map[string]interface{}。在反向解碼時,我們可以為某些字段設(shè)置mapstructure:",omitempty"。這樣當(dāng)這些字段為默認(rèn)值時,就不會出現(xiàn)在結(jié)構(gòu)的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 style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">Job字段設(shè)置了mapstructure:",omitempty",且對象p的Job字段未設(shè)置。運行結(jié)果:
$ go run main.go
{"Age":18,"Name":"dj"}
Metadata
解碼時會產(chǎn)生一些有用的信息,mapstructure可以使用Metadata收集這些信息。Metadata結(jié)構(gòu)如下:
//?mapstructure.go
type?Metadata?struct?{
??Keys???[]string
??Unused?[]string
}
Metadata只有兩個導(dǎo)出字段:
Keys:解碼成功的鍵名;Unused:在源數(shù)據(jù)中存在,但是目標(biāo)結(jié)構(gòu)中不存在的鍵名。
為了收集這些數(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結(jié)構(gòu),傳入DecodeMetadata收集解碼的信息。運行結(jié)果:
$ go run main.go
keys:[]string{"Name", "Age"} unused:[]string{"job"}
錯誤處理
mapstructure執(zhí)行轉(zhuǎn)換的過程中不可避免地會產(chǎn)生錯誤,例如 JSON 中某個鍵的類型與對應(yīng) Go 結(jié)構(gòu)體中的字段類型不一致。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())
??}
}
上面代碼中,結(jié)構(gòu)體中Person中字段Name為string類型,但輸入中name為int類型;字段Age為int類型,但輸入中age為string類型;字段Emails為[]string類型,但輸入中emails為[]int類型。故Decode返回錯誤。運行結(jié)果:
$ 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'
從錯誤信息中很容易看出哪里出錯了。
弱類型輸入
有時候,我們并不想對結(jié)構(gòu)體字段類型和map[string]interface{}的對應(yīng)鍵值做強(qiáng)類型一致的校驗。這時可以使用WeakDecode/WeakDecodeMetadata方法,它們會嘗試做類型轉(zhuǎn)換:
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對應(yīng)的值123是int類型,但是在WeakDecode中會將其轉(zhuǎn)換為string類型以匹配Person.Name字段的類型。同樣的,age的值"18"是string類型,在WeakDecode中會將其轉(zhuǎn)換為int類型以匹配Person.Age字段的類型。需要注意一點,如果類型轉(zhuǎn)換失敗了,WeakDecode同樣會返回錯誤。例如將上例中的age設(shè)置為"bad value",它就不能轉(zhuǎn)為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時,如果輸入中的鍵值沒有與之對應(yīng)的字段就返回錯誤;ZeroFields:為true時,在Decode前清空目標(biāo)map。為false時,則執(zhí)行的是map的合并。用在struct到map的轉(zhuǎn)換中;WeaklyTypedInput:實現(xiàn)WeakDecode/WeakDecodeMetadata的功能;Metadata:不為nil時,收集Metadata數(shù)據(jù);Result:為結(jié)果對象,在map到struct的轉(zhuǎn)換中,Result為struct類型。在struct到map的轉(zhuǎn)換中,Result為map類型;TagName:默認(rèn)使用mapstructure作為結(jié)構(gòu)體的標(biāo)簽名,可以通過該字段設(shè)置。
看示例:
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內(nèi)部就是通過這種方式實現(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內(nèi)部都是先設(shè)置DecoderConfig的對應(yīng)字段,然后創(chuàng)建Decoder對象,最后調(diào)用其Decode方法實現(xiàn)的。
總結(jié)
mapstructure實現(xiàn)優(yōu)雅,功能豐富,代碼結(jié)構(gòu)清晰,非常推薦一看!
大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue?
參考
mapstructure GitHub:https://github.com/mitchellh/mapstructure Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關(guān)注
