简介
mapstructure
用于将通用的 map[string]interface{}
解码到对应的 Go 构造体中,或者执行相同的操作。很多时候,解析来自多种源头的数据流时,咱们个别当时并不知道他们对应的具体类型。只有读取到一些字段之后能力做出判断。这时,咱们能够先应用规范的 encoding/json
库将数据解码为 map[string]interface{}
类型,而后依据标识字段利用 mapstructure
库转为相应的 Go 构造体以便应用。
疾速应用
本文代码采纳 Go Modules。
首先创立目录并初始化:
$ 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
,他们的字段有些许不同。当初,咱们约定通信的 JSON 串中有一个 type
字段。当 type
的值为 person
时,该 JSON 串示意的是 Person
类型的数据。当 type
的值为 cat
时,该 JSON 串示意的是 Cat
类型的数据。
下面代码中,咱们先用 json.Unmarshal
将字节流解码为 map[string]interface{}
类型。而后读取外面的 type
字段。依据 type
字段的值,再应用 mapstructure.Decode
将该 JSON 串别离解码为 Person
和Cat
类型的值,并输入。
实际上,Google Protobuf 通常也应用这种形式。在协定中增加音讯 ID 或 全限定音讯名 。接管方收到数据后,先读取协定 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
构造的数据要在 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 中对应的值 同时设置到这两个字段中,即这两个字段有雷同的值。
未映射的值
如果源数据中有未映射的值(即构造体中无对应的字段),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)
}
下面代码中,咱们为构造体定义了一个 Other
字段,用于保留未映射的键值。输入后果:
other map[handsome:true height:1.8m]
逆向转换
后面咱们都是将 map[string]interface{}
解码到 Go 构造体中。mapstructure
当然也能够将 Go 构造体反向解码为 map[string]interface{}
。在反向解码时,咱们能够为某些字段设置mapstructure:",omitempty"
。这样当这些字段为默认值时,就不会呈现在构造的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))
}
下面代码中,咱们为 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
:在源数据中存在,然而指标构造中不存在的键名。
为了收集这些数据,咱们须要应用 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
执行转换的过程中不可避免地会产生谬误,例如 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
实现下面介绍的任何性能:
// mapstructure.go
type DecoderConfig struct {
ErrorUnused bool
ZeroFields bool
WeaklyTypedInput bool
Metadata *Metadata
Result interface{}
TagName string
}
各个字段含意如下:
ErrorUnused
:为true
时,如果输出中的键值没有与之对应的字段就返回谬误;ZeroFields
:为true
时,在Decode
前清空指标map
。为false
时,则执行的是map
的合并。用在struct
到map
的转换中;WeaklyTypedInput
:实现WeakDecode/WeakDecodeMetadata
的性能;Metadata
:不为nil
时,收集Metadata
数据;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
的形式实现了后面弱类型输出大节中的示例代码。实际上 WeakDecode
外部就是通过这种形式实现的,上面是 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
的对应字段,而后创立 Decoder
对象,最初调用其 Decode
办法实现的。
总结
mapstructure
实现优雅,功能丰富,代码构造清晰,十分举荐一看!
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue????
参考
- mapstructure GitHub:https://github.com/mitchellh/mapstructure
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~