Go 每日一库之 mapstructure
简介
https://blog.csdn.net/qq_40530622/article/details/119408623
https://blog.csdn.net/ma_jiang/article/details/120720840
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