简介
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】,独特学习,一起提高~
发表回复