golang mapstructure庫的具體使用
簡(jiǎn)介
mapstructure用于將通用的map[string]interface{}
解碼到對(duì)應(yīng)的 Go 結(jié)構(gòu)體中,或者執(zhí)行相反的操作。很多時(shí)候,解析來自多種源頭的數(shù)據(jù)流時(shí),我們一般事先并不知道他們對(duì)應(yīng)的具體類型。只有讀取到一些字段之后才能做出判斷。這時(shí),我們可以先使用標(biāo)準(zhǔn)的encoding/json
庫將數(shù)據(jù)解碼為map[string]interface{}
類型,然后根據(jù)標(biāo)識(shí)字段利用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) } } }
運(yùn)行結(jié)果:
$ go run main.go person {dj 18 programmer} cat {kitty 1 Ragdoll}
我們定義了兩個(gè)結(jié)構(gòu)體Person
和Cat
,他們的字段有些許不同?,F(xiàn)在,我們約定通信的 JSON 串中有一個(gè)type
字段。當(dāng)type
的值為person
時(shí),該 JSON 串表示的是Person
類型的數(shù)據(jù)。當(dāng)type
的值為cat
時(shí),該 JSON 串表示的是Cat
類型的數(shù)據(jù)。
上面代碼中,我們先用json.Unmarshal
將字節(jié)流解碼為map[string]interface{}
類型。然后讀取里面的type
字段。根據(jù)type
字段的值,再使用mapstructure.Decode
將該 JSON 串分別解碼為Person
和Cat
類型的值,并輸出。
實(shí)際上,Google Protobuf 通常也使用這種方式。在協(xié)議中添加消息 ID 或全限定消息名。接收方收到數(shù)據(jù)后,先讀取協(xié)議 ID 或全限定消息名。然后調(diào)用 Protobuf 的解碼方法將其解碼為對(duì)應(yīng)的Message
結(jié)構(gòu)。從這個(gè)角度來看,mapstructure
也可以用于網(wǎng)絡(luò)消息解碼,如果你不考慮性能的話?。
字段標(biāo)簽
默認(rèn)情況下,mapstructure
使用結(jié)構(gòu)體中字段的名稱做這個(gè)映射,例如我們的結(jié)構(gòu)體有一個(gè)Name
字段,mapstructure
解碼時(shí)會(huì)在map[string]interface{}
中查找鍵名name
。注意,這里的name
是大小寫不敏感的!
type Person struct { Name string }
當(dāng)然,我們也可以指定映射的字段名。為了做到這一點(diǎn),我們需要為字段設(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
才能正確解析。另外,注意到,我們將第二個(gè) JSON 串中的Age
和第三個(gè) JSON 串中的Name
首字母大寫了,但是并沒有影響解碼結(jié)果。mapstructure
處理字段映射是大小寫不敏感的。
內(nèi)嵌結(jié)構(gòu)
結(jié)構(gòu)體可以任意嵌套,嵌套的結(jié)構(gòu)被認(rèn)為是擁有該結(jié)構(gòu)體名字的另一個(gè)字段。例如,下面兩種Friend
的定義方式對(duì)于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) } } }
注意對(duì)比Friend1
和Friend2
使用的 JSON 串的不同。
另外需要注意一點(diǎn),如果父結(jié)構(gòu)體中有同名的字段,那么mapstructure
會(huì)將JSON 中對(duì)應(yīng)的值同時(shí)設(shè)置到這兩個(gè)字段中,即這兩個(gè)字段有相同的值。
未映射的值
如果源數(shù)據(jù)中有未映射的值(即結(jié)構(gòu)體中無對(duì)應(yīng)的字段),mapstructure
默認(rèn)會(huì)忽略它。
我們可以在結(jié)構(gòu)體中定義一個(gè)字段,為其設(shè)置mapstructure:",remain"
標(biāo)簽。這樣未映射的值就會(huì)添加到這個(gè)字段中。注意,這個(gè)字段的類型只能為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)體定義了一個(gè)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í),我們可以為某些字段設(shè)置mapstructure:",omitempty"
。這樣當(dāng)這些字段為默認(rèn)值時(shí),就不會(huì)出現(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>Job字段設(shè)置了mapstructure:",omitempty"
,且對(duì)象p
的Job
字段未設(shè)置。運(yùn)行結(jié)果:
$ go run main.go {"Age":18,"Name":"dj"}
Metadata
解碼時(shí)會(huì)產(chǎn)生一些有用的信息,mapstructure
可以使用Metadata
收集這些信息。Metadata
結(jié)構(gòu)如下:
// mapstructure.go type Metadata struct { Keys []string Unused []string }
Metadata
只有兩個(gè)導(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) }
先定義一個(gè)Metadata
結(jié)構(gòu),傳入DecodeMetadata
收集解碼的信息。運(yùn)行結(jié)果:
$ go run main.go keys:[]string{"Name", "Age"} unused:[]string{"job"}
錯(cuò)誤處理
mapstructure
執(zhí)行轉(zhuǎn)換的過程中不可避免地會(huì)產(chǎn)生錯(cuò)誤,例如 JSON 中某個(gè)鍵的類型與對(duì)應(yīng) Go 結(jié)構(gòu)體中的字段類型不一致。Decode/DecodeMetadata
會(huì)返回這些錯(cuò)誤:
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
返回錯(cuò)誤。運(yùn)行結(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'
從錯(cuò)誤信息中很容易看出哪里出錯(cuò)了。
弱類型輸入
有時(shí)候,我們并不想對(duì)結(jié)構(gòu)體字段類型和map[string]interface{}
的對(duì)應(yīng)鍵值做強(qiáng)類型一致的校驗(yàn)。這時(shí)可以使用WeakDecode/WeakDecodeMetadata
方法,它們會(huì)嘗試做類型轉(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
對(duì)應(yīng)的值123
是int
類型,但是在WeakDecode
中會(huì)將其轉(zhuǎn)換為string
類型以匹配Person.Name
字段的類型。同樣的,age
的值"18"
是string
類型,在WeakDecode
中會(huì)將其轉(zhuǎn)換為int
類型以匹配Person.Age
字段的類型。
需要注意一點(diǎn),如果類型轉(zhuǎn)換失敗了,WeakDecode
同樣會(huì)返回錯(cuò)誤。例如將上例中的age
設(shè)置為"bad value"
,它就不能轉(zhuǎn)為int
類型,故而返回錯(cuò)誤。
解碼器
除了上面介紹的方法外,mapstructure
還提供了更靈活的解碼器(Decoder
)??梢酝ㄟ^配置DecoderConfig
實(shí)現(xiàn)上面介紹的任何功能:
// mapstructure.go type DecoderConfig struct { ErrorUnused bool ZeroFields bool WeaklyTypedInput bool Metadata *Metadata Result interface{} TagName string }
各個(gè)字段含義如下:
ErrorUnused
:為true
時(shí),如果輸入中的鍵值沒有與之對(duì)應(yīng)的字段就返回錯(cuò)誤;ZeroFields
:為true
時(shí),在Decode
前清空目標(biāo)map
。為false
時(shí),則執(zhí)行的是map
的合并。用在struct
到map
的轉(zhuǎn)換中;WeaklyTypedInput
:實(shí)現(xiàn)WeakDecode/WeakDecodeMetadata
的功能;Metadata
:不為nil
時(shí),收集Metadata
數(shù)據(jù);Result
:為結(jié)果對(duì)象,在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
的方式實(shí)現(xiàn)了前面弱類型輸入小節(jié)中的示例代碼。實(shí)際上WeakDecode
內(nèi)部就是通過這種方式實(shí)現(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) }
再實(shí)際上,Decode/DecodeMetadata/WeakDecodeMetadata
內(nèi)部都是先設(shè)置DecoderConfig
的對(duì)應(yīng)字段,然后創(chuàng)建Decoder
對(duì)象,最后調(diào)用其Decode
方法實(shí)現(xiàn)的。
總結(jié)
mapstructure
實(shí)現(xiàn)優(yōu)雅,功能豐富,代碼結(jié)構(gòu)清晰,非常推薦一看!
到此這篇關(guān)于golang mapstructure庫的具體使用的文章就介紹到這了,更多相關(guān)go mapstructure庫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言sync.Pool對(duì)象池使用場(chǎng)景基本示例
這篇文章主要為大家介紹了Go語言sync.Pool對(duì)象池使用場(chǎng)景的基本示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Go語言實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)和重啟的示例詳解
優(yōu)雅的關(guān)機(jī)是指在關(guān)閉服務(wù)之前,先讓服務(wù)處理完當(dāng)前正在處理的請(qǐng)求,然后再關(guān)閉服務(wù),本文主要為大家詳細(xì)介紹了如何使用Go語言實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)和重啟,感興趣的小伙伴可以參考一下2025-04-04linux中用shell快速安裝配置Go語言的開發(fā)環(huán)境
相信每位開發(fā)者都知道選擇一門開發(fā)語言,免不了需要安裝配置開發(fā)環(huán)境,所以這篇文章給大家分享了linux下使用shell一鍵安裝配置GO語言開發(fā)環(huán)境的方法,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-10-10深入了解Golang網(wǎng)絡(luò)編程N(yùn)et包的使用
net包主要是增加?context?控制,封裝了一些不同的連接類型以及DNS?查找等等,同時(shí)在有需要的地方引入?goroutine?提高處理效率。本文主要和大家分享下在Go中網(wǎng)絡(luò)編程的實(shí)現(xiàn),需要的可以參考一下2022-07-07Golang實(shí)現(xiàn)事務(wù)型內(nèi)存數(shù)據(jù)庫的方法詳解
內(nèi)存數(shù)據(jù)庫經(jīng)我們經(jīng)常用到,例如Redis,那么如何從零實(shí)現(xiàn)一個(gè)內(nèi)存數(shù)據(jù)庫呢,本文旨在介紹如何使用Golang編寫一個(gè)KV內(nèi)存數(shù)據(jù)庫MossDB2023-03-03