golang 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,他們的字段有些許不同。現(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>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)清晰,非常推薦一看!
到此這篇關(guān)于golang mapstructure庫的具體使用的文章就介紹到這了,更多相關(guān)go mapstructure庫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言實現(xiàn)優(yōu)雅關(guān)機(jī)和重啟的示例詳解
優(yōu)雅的關(guān)機(jī)是指在關(guān)閉服務(wù)之前,先讓服務(wù)處理完當(dāng)前正在處理的請求,然后再關(guān)閉服務(wù),本文主要為大家詳細(xì)介紹了如何使用Go語言實現(xiàn)優(yōu)雅關(guān)機(jī)和重啟,感興趣的小伙伴可以參考一下2025-04-04
linux中用shell快速安裝配置Go語言的開發(fā)環(huán)境
相信每位開發(fā)者都知道選擇一門開發(fā)語言,免不了需要安裝配置開發(fā)環(huán)境,所以這篇文章給大家分享了linux下使用shell一鍵安裝配置GO語言開發(fā)環(huán)境的方法,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-10-10
深入了解Golang網(wǎng)絡(luò)編程Net包的使用
net包主要是增加?context?控制,封裝了一些不同的連接類型以及DNS?查找等等,同時在有需要的地方引入?goroutine?提高處理效率。本文主要和大家分享下在Go中網(wǎng)絡(luò)編程的實現(xiàn),需要的可以參考一下2022-07-07
Golang實現(xiàn)事務(wù)型內(nèi)存數(shù)據(jù)庫的方法詳解
內(nèi)存數(shù)據(jù)庫經(jīng)我們經(jīng)常用到,例如Redis,那么如何從零實現(xiàn)一個內(nèi)存數(shù)據(jù)庫呢,本文旨在介紹如何使用Golang編寫一個KV內(nèi)存數(shù)據(jù)庫MossDB2023-03-03

