使用 Mapstructure 解析 Json,你學(xué)會(huì)了嗎?
背景
前幾天群里的小伙伴問(wèn)了一個(gè)這樣的問(wèn)題:
圖片
其實(shí)質(zhì)就是在面對(duì) value 類型不確定的情況下,怎么解析這個(gè) json?
我下意識(shí)就想到了 [mapstructure](https://github.com/mitchellh/mapstructure) 這個(gè)庫(kù),它可以幫助我們類似 PHP 那樣去處理弱類型的結(jié)構(gòu)。
介紹
先來(lái)介紹一下 mapstructure 這個(gè)庫(kù)主要用來(lái)做什么的吧,官網(wǎng)是這么介紹的:
mapstructure 是一個(gè) Go 庫(kù),用于將通用映射值解碼為結(jié)構(gòu),反之亦然,同時(shí)提供有用的錯(cuò)誤處理。
該庫(kù)在解碼數(shù)據(jù)流(JSON、Gob 等)中的值時(shí)最為有用,因?yàn)樵谧x取部分?jǐn)?shù)據(jù)之前,您并不十分清楚底層數(shù)據(jù)的結(jié)構(gòu)。因此,您可以讀取 map[string]interface{} 并使用此庫(kù)將其解碼為適當(dāng)?shù)谋镜?Go 底層結(jié)構(gòu)。
簡(jiǎn)單來(lái)說(shuō),它擅長(zhǎng)解析一些我們并不十分清楚底層數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)流到我們定義的結(jié)構(gòu)體中。
下面我們通過(guò)幾個(gè)例子來(lái)簡(jiǎn)單介紹一下 mapstructure 怎么使用。
例子
普通形式
func normalDecode() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
// 此輸入可以來(lái)自任何地方,但通常來(lái)自諸如解碼 JSON 之類的東西,我們最初不太確定結(jié)構(gòu)。
input := map[string]interface{}{
"name": "Tim",
"age": 31,
"emails": []string{"one@gmail.com", "two@gmail.com", "three@gmail.com"},
"extra": map[string]string{
"twitter": "Tim",
},
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
輸出:
main.Person{Name:"Tim", Age:31, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Tim"}}
這個(gè)方式應(yīng)該是我們最經(jīng)常使用的,非常簡(jiǎn)單的將 map[string]interface{} 映射到我們的結(jié)構(gòu)體中。
在這里,我們并沒(méi)有指定每個(gè) field 的 tag,讓 mapstructure 自動(dòng)去映射。
如果我們的 input 是一個(gè) json 字符串,那么我們需要將 json 字符串解析為 map[string]interface{} 之后,再將其映射到我們的結(jié)構(gòu)體中。
func jsonDecode() {
var jsonStr = `{
"name": "Tim",
"age": 31,
"gender": "male"
}`
type Person struct {
Name string
Age int
Gender string
}
m := make(map[string]interface{})
err := json.Unmarshal([]byte(jsonStr), &m)
if err != nil {
panic(err)
}
var result Person
err = mapstructure.Decode(m, &result)
if err != nil {
panic(err.Error())
}
fmt.Printf("%#v\n", result)
}
輸出:
main.Person{Name:"Tim", Age:31, Gender:"male"}
嵌入式結(jié)構(gòu)
mapstructure 允許我們壓縮多個(gè)嵌入式結(jié)構(gòu),并通過(guò) squash 標(biāo)簽進(jìn)行處理。
func embeddedStructDecode() {
// 使用 squash 標(biāo)簽允許壓縮多個(gè)嵌入式結(jié)構(gòu)。通過(guò)創(chuàng)建多種類型的復(fù)合結(jié)構(gòu)并對(duì)其進(jìn)行解碼來(lái)演示此功能。
type Family struct {
LastName string
}
type Location struct {
City string
}
type Person struct {
Family `mapstructure:",squash"`
Location `mapstructure:",squash"`
FirstName string
}
input := map[string]interface{}{
"FirstName": "Tim",
"LastName": "Liu",
"City": "China, Guangdong",
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%s %s, %s\n", result.FirstName, result.LastName, result.City)
}
輸出:
Tim Liu, China, Guangdong
在這個(gè)例子中, Person 里面有著 Location 和 Family 的嵌入式結(jié)構(gòu)體,通過(guò) squash 標(biāo)簽進(jìn)行壓縮,從而達(dá)到平鋪的作用。
元數(shù)據(jù)
func metadataDecode() {
type Person struct {
Name string
Age int
Gender string
}
// 此輸入可以來(lái)自任何地方,但通常來(lái)自諸如解碼 JSON 之類的東西,我們最初不太確定結(jié)構(gòu)。
input := map[string]interface{}{
"name": "Tim",
"age": 31,
"email": "one@gmail.com",
}
// 對(duì)于元數(shù)據(jù),我們制作了一個(gè)更高級(jí)的 DecoderConfig,以便我們可以更細(xì)致地配置所使用的解碼器。在這種情況下,我們只是告訴解碼器我們想要跟蹤元數(shù)據(jù)。
var md mapstructure.Metadata
var result Person
config := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &result,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
panic(err)
}
if err = decoder.Decode(input); err != nil {
panic(err)
}
fmt.Printf("value: %#v, keys: %#v, Unused keys: %#v, Unset keys: %#v\n", result, md.Keys, md.Unused, md.Unset)
}
輸出:
value: main.Person{Name:"Tim", Age:31, Gender:""}, keys: []string{"Name", "Age"}, Unused keys: []string{"email"}, Unset keys: []string{"Gender"}
從這個(gè)例子我們可以看出,使用 Metadata 可以記錄我們結(jié)構(gòu)體以及 map[string]interface{} 的差異,相同的部分會(huì)正確映射到對(duì)應(yīng)的字段中,而差異則使用了 Unused 和 Unset 來(lái)表達(dá)。
- Unused:map 中有著結(jié)構(gòu)體所沒(méi)有的字段。
- Unset:結(jié)構(gòu)體中有著 map 中所沒(méi)有的字段。
避免空值的映射
這里的使用其實(shí)和內(nèi)置的 json 庫(kù)使用方式是一樣的,都是借助 omitempty 標(biāo)簽來(lái)解決。
func omitemptyDecode() {
// 添加 omitempty 注釋以避免空值的映射鍵
type Family struct {
LastName string
}
type Location struct {
City string
}
type Person struct {
*Family `mapstructure:",omitempty"`
*Location `mapstructure:",omitempty"`
Age int
FirstName string
}
result := &map[string]interface{}{}
input := Person{FirstName: "Somebody"}
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", result)
}
輸出:
&map[Age:0 FirstName:Somebody]
這里我們可以看到 *Family 和 *Location 都被設(shè)置了 omitempty,所以在解析過(guò)程中會(huì)忽略掉空值。而 Age 沒(méi)有設(shè)置,并且 input 中沒(méi)有對(duì)應(yīng)的 value,所以在解析中使用對(duì)應(yīng)類型的零值來(lái)表達(dá),而 int 類型的零值就是 0。
剩余字段
func remainDataDecode() {
type Person struct {
Name string
Age int
Other map[string]interface{} `mapstructure:",remain"`
}
input := map[string]interface{}{
"name": "Tim",
"age": 31,
"email": "one@gmail.com",
"gender": "male",
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
輸出:
main.Person{Name:"Tim", Age:31, Other:map[string]interface {}{"email":"one@gmail.com", "gender":"male"}}
從代碼可以看到 Other 字段被設(shè)置了 remain,這意味著 input 中沒(méi)有正確映射的字段都會(huì)被放到 Other 中,從輸出可以看到,email 和 gender 已經(jīng)被正確的放到 Other 中了。
自定義標(biāo)簽
func tagDecode() {
// 請(qǐng)注意,結(jié)構(gòu)類型中定義的 mapstructure 標(biāo)簽可以指示將值映射到哪些字段。
type Person struct {
Name string `mapstructure:"person_name"`
Age int `mapstructure:"person_age"`
}
input := map[string]interface{}{
"person_name": "Tim",
"person_age": 31,
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
輸出:
main.Person{Name:"Tim", Age:31}
在 Person 結(jié)構(gòu)中,我們將 person_name 和 person_age 分別映射到 Name 和 Age 中,從而達(dá)到在不破壞結(jié)構(gòu)的基礎(chǔ)上,去正確的解析。
弱類型解析
正如前面所說(shuō),mapstructure 提供了類似 PHP 解析弱類型結(jié)構(gòu)的方法。
func weaklyTypedInputDecode() {
type Person struct {
Name string
Age int
Emails []string
}
// 此輸入可以來(lái)自任何地方,但通常來(lái)自諸如解碼 JSON 之類的東西,由 PHP 等弱類型語(yǔ)言生成。
input := map[string]interface{}{
"name": 123, // number => string
"age": "31", // string => number
"emails": map[string]interface{}{}, // empty map => empty array
}
var result Person
config := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: &result,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
panic(err)
}
err = decoder.Decode(input)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
輸出:
main.Person{Name:"123", Age:31, Emails:[]string{}}
從代碼可以看到,input 中的 name、age 和 Person 結(jié)構(gòu)體中的 Name、Age 類型不一致,而 email 更是離譜,一個(gè)字符串?dāng)?shù)組,一個(gè)是 map。
但是我們通過(guò)自定義 DecoderConfig,將 WeaklyTypedInput 設(shè)置成 true 之后,mapstructure 很容易幫助我們解決這類弱類型的解析問(wèn)題。
但是也不是所有問(wèn)題都能解決,通過(guò)源碼我們可以知道有如下限制:
// - bools to string (true = "1", false = "0")
// - numbers to string (base 10)
// - bools to int/uint (true = 1, false = 0)
// - strings to int/uint (base implied by prefix)
// - int to bool (true if value != 0)
// - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F,
// FALSE, false, False. Anything else is an error)
// - empty array = empty map and vice versa
// - negative numbers to overflowed uint values (base 10)
// - slice of maps to a merged map
// - single values are converted to slices if required. Each
// element is weakly decoded. For example: "4" can become []int{4}
// if the target type is an int slice.
大家使用這種弱類型解析的時(shí)候也需要注意。
錯(cuò)誤處理
mapstructure 錯(cuò)誤提示非常的友好,下面我們來(lái)看看遇到錯(cuò)誤時(shí),它是怎么提示的。
func decodeErrorHandle() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
input := map[string]interface{}{
"name": 123,
"age": "bad value",
"emails": []int{1, 2, 3},
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
fmt.Println(err.Error())
}
}
輸出:
5 error(s) decoding:
* 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value'
* 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1'
* 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2'
* 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3'
* 'Name' expected type 'string', got unconvertible type 'int', value: '123'
這里的錯(cuò)誤提示會(huì)告訴我們每個(gè)字段,字段里的值應(yīng)該需要怎么表達(dá),我們可以通過(guò)這些錯(cuò)誤提示,比較快的去修復(fù)問(wèn)題。
總結(jié)
從上面這些例子看看到 mapstructure 的強(qiáng)大之處,很好的幫我們解決了實(shí)實(shí)在在的問(wèn)題,也在節(jié)省我們的開(kāi)發(fā)成本。
但是從源碼來(lái)看,內(nèi)部使用了大量的反射,這可能會(huì)對(duì)一些特殊場(chǎng)景帶來(lái)性能隱患。所以大家在使用的時(shí)候,一定要充分考慮產(chǎn)品邏輯以及場(chǎng)景。
以下貼一小段刪減過(guò)的源碼:
// Decode decodes the given raw interface to the target pointer specified
// by the configuration.
func (d *Decoder) Decode(input interface{}) error {
return d.decode("", input, reflect.ValueOf(d.config.Result).Elem())
}
// Decodes an unknown data type into a specific reflection value.
func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error {
....
var err error
outputKind := getKind(outVal)
addMetaKey := true
switch outputKind {
case reflect.Bool:
err = d.decodeBool(name, input, outVal)
case reflect.Interface:
err = d.decodeBasic(name, input, outVal)
case reflect.String:
err = d.decodeString(name, input, outVal)
case reflect.Int:
err = d.decodeInt(name, input, outVal)
case reflect.Uint:
err = d.decodeUint(name, input, outVal)
case reflect.Float32:
err = d.decodeFloat(name, input, outVal)
case reflect.Struct:
err = d.decodeStruct(name, input, outVal)
case reflect.Map:
err = d.decodeMap(name, input, outVal)
case reflect.Ptr:
addMetaKey, err = d.decodePtr(name, input, outVal)
case reflect.Slice:
err = d.decodeSlice(name, input, outVal)
case reflect.Array:
err = d.decodeArray(name, input, outVal)
case reflect.Func:
err = d.decodeFunc(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return fmt.Errorf("%s: unsupported type: %s", name, outputKind)
}
// If we reached here, then we successfully decoded SOMETHING, so
// mark the key as used if we're tracking metainput.
if addMetaKey && d.config.Metadata != nil && name != "" {
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
}
return err
}