字典解析库
mapstructure 库
将 map[string]interface{} 数据解析到结构体中
基础使用(推荐做法:始终使用标签)
相关的包:
1
2
3
import {
"github.com/mitchellh/mapstructure"
}
语法
1
2
3
4
5
6
7
8
9
10
mapstructure.Decode(待解析map数据, &目标)
// 等价
bytes, err := json.Marshal(待解析map数据)
if err != nil {
return nil, fmt.Errorf("无法序列化输入参数: %w", err)
}
if err := json.Unmarshal(bytes, &目标); err != nil {
return nil, fmt.Errorf("无法反序列化输入参数到TaskInput: %w", err)
}
在日常开发中,接受的数据可能不是固定的格式,而是会根据某个值的不同有不同的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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() {
// json数据
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) //将JSON字符串反序列化为 map[string]interface{}
if err != nil {
log.Fatal(err)
}
//
switch m["type"].(string) {
case "person":
var p Person
mapstructure.Decode(m, &p) // 用于将 map[string]interface{} 数据解析到结构体中
fmt.Println("person:", p)
case "cat":
var cat Cat
mapstructure.Decode(m, &cat)
fmt.Println("cat:", cat)
}
}
}
运行结果:
1
2
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也可以用于网络消息解码,如果你不考虑性能的话。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type ShopIdGray1 struct {
Data struct {
PlatUserIds map[string]bool `json:"plat_user_ids" mapstructure:"plat_user_ids"`
IsComplete bool `json:"is_complete" mapstructure:"is_complete"`
} `json:"data"`
}
func Test2(t *testing.T) {
platUserID := map[string]bool{
"1212231": true,
}
// 构建 data
data := map[string]interface{}{
"plat_user_ids": platUserID,
"is_complete": false,
}
// 方式1
p1 := &ShopIdGray1{} // 指针
err := mapstructure.Decode(data, &p1.Data) //进行解码,使用 mapstructure 将 data 解码到 gray.Data
if err != nil {
fmt.Println("Error decoding data:", err)
return
}
fmt.Printf("Decoded p1: %+v\n", p1)
fmt.Printf("Decoded p1.Data: %+v\n", p1.Data)
fmt.Println(p1.Data.PlatUserIds["1212231"])
//方式2
p2 := ShopIdGray1{} // 值
err = mapstructure.Decode(data, &p2.Data) //进行解码,使用 mapstructure 将 data 解码到 gray.Data
if err != nil {
return
}
fmt.Printf("Decoded p2: %+v\n", p2)
fmt.Printf("Decoded p2.Data: %+v\n", p2.Data)
fmt.Println(p2.Data.PlatUserIds["1212231"])
}
1
2
3
4
5
6
Decoded ShopIdGray1: &{Data:{PlatUserIds:map[1212231:true] IsComplete:false}}
Decoded ShopIdGray1.Data: {PlatUserIds:map[1212231:true] IsComplete:false}
true
Decoded ShopIdGray1: {Data:{PlatUserIds:map[1212231:true] IsComplete:false}}
Decoded ShopIdGray1.Data: {PlatUserIds:map[1212231:true] IsComplete:false}
true
默认值
当 mapstructure.Decode 解码时,如果 map 中缺少某些字段,结构体字段会使用 Go 的零值. 规则和json库一致
使用标签
1. 字段名不一致时
1
2
3
4
5
type Person struct {
FullName string `mapstructure:"name"` // JSON 是 "name",结构体是 "FullName"
Age int `mapstructure:"age"`
JobTitle string `mapstructure:"job"` // JSON 是 "job",结构体是 "JobTitle"
}
2. 需要忽略某些字段时
1
2
3
4
5
6
type Person struct {
Name string `mapstructure:"name"`
Age int `mapstructure:"age"`
Job string `mapstructure:"job"`
InternalID int `mapstructure:"-"` // 忽略这个字段,不解码
}
3. 需要设置默认值或元数据时
1
2
3
4
type Person struct {
Name string `mapstructure:"name" json:"name"` // 同时兼容多个库
Age int `mapstructure:"age" json:"age" validate:"gte=0,lte=150"`
}
4. 嵌入式结构体或复杂映射时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Config struct {
Server struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
} `mapstructure:"server"` // 映射到嵌套的 "server" 字段
Database struct {
URL string `mapstructure:"url"`
} `mapstructure:"database"`
}
// 对应 JSON:
// {
// "server": { "host": "localhost", "port": 8080 },
// "database": { "url": "postgres://..." }
// }
详细使用
Field Tags (字段标签)
默认情况下,mapstructure使用结构体中字段的名称做这个映射,例如我们的结构体有一个Name字段,mapstructure解码时会在map[string]interface{}中查找键名name。
注意,这里的name是大小写不敏感的!
1
2
3
type Person struct {
Name string
}
当然,我们也可以指定映射的字段名。设置mapstructure标签。例如下面使用username代替上例中的name:
1
2
3
type Person struct {
Name string `mapstructure:"username"`
}
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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", // 字段映射为username
"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处理字段映射是大小写不敏感的。
Renaming Fields
在实际使用过程中,我们可能需要重命名 mapstructure 查找的键,这个时候,可以使用 “mapstructure” 标签并直接设置一个值。例如,要将上面的 “username” 示例更改为 “user”:
1
2
3
type User struct {
Username string `mapstructure:"user"`
}
Embedded Structs and Squashing(内嵌结构)
结构体可以任意嵌套,嵌套的结构被认为是拥有该结构体名字的另一个字段。
方式1
1
2
3
type Friend struct {
Person
}
对应map
1
2
3
map[string]interface{} {
"person": map[string]interface{}{"name": "dj"},
}
方式2
1
2
3
type Friend struct {
Person `mapstructure:",squash"` //同样被 squash 到顶层 // 一般
}
对应map
1
2
3
map[string]interface{}{
"name": "dj",
}
看例子1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/mitchellh/mapstructure"
)
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)
}
}
}
结果:
1
2
3
friend1
friend2
Exiting.
注意对比Friend1和Friend2使用的 JSON 串的不同。
接着看这个例子2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/mitchellh/mapstructure"
)
type Person struct {
Name string
Type string
}
type Friend1 struct {
Type string
Person
}
type Friend2 struct {
Type string
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.Printf("friend1: %+v \n", f1)
case "friend2":
var f2 Friend2
mapstructure.Decode(m, &f2)
fmt.Printf("friend2: %+v \n", f2)
}
}
}
结果:
1
2
friend1: {Type:friend1 Person:{Name:dj Type:}}
friend2: {Type:friend2 Person:{Name:dj2 Type:friend2}}
注意对比Friend1和Friend2使用的 JSON 串的不同。
另外需要注意一点,如果父结构体中有同名的字段,那么mapstructure会将JSON 中对应的值同时设置到这两个字段中,即这两个字段有相同的值。
Remainder Values (未映射的值)
如果源数据中有未映射的值(即结构体中无对应的字段),mapstructure默认会忽略它。
解决方法:
- 可以通过在
DecoderConfig中设置ErrorUnused来引发错误。如果正在使用元数据(Metadata),还可以维护一个未使用键的切片(slice)。 - 可以在结构体中定义一个字段,为其设置
mapstructure:",remain"标签。这样未映射的值就会添加到这个字段中。注意,这个字段的类型只能为map[string]interface{}或map[interface{}]interface{}这两种类型之一。
注意: 这个字段的类型只能为map[string]interface{}或map[interface{}]interface{}这两种类型之一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 struct {
Name string
Age int
Job string
Other map[string]interface{} `mapstructure:",remain"` //这个字段的类型只能为map[string]interface{}或map[interface{}]interface{}这两种类型之一
}
mapstructure.Decode(m, &p)
// p: {Name:dj Age:18 Job:programmer Other:map[handsome:true height:1.8m]}
fmt.Printf("p: %+v", p)
}
Unexported fields
Go 中规定了 未导出的(私有的)结构体字段不能在定义它们的包之外进行设置,解码器将直接跳过它们。
通过以下例子来进行讲解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
type Exported struct {
private string // 首字母小写就表示私有的
Public string
}
func main() {
m := map[string]interface{}{
"private": "I will be ignored",
"Public": "I made it through!",
}
var e Exported
_ = mapstructure.Decode(m, &e)
fmt.Printf("e: %+v", e)
}
结果:
1
e: {private: Public:I made it through!}
Other Configuration
mapstructure是高度可配置的。有关支持的其他功能和选项,请参阅 DecoderConfig 结构。
逆向转换
在反向解码时,可以为某些字段设置mapstructure:",omitempty"。这样当这些字段为空值时,就不会出现在结构的map[string]interface{}中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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))
}
结果:
1
2
$ go run main.go
{"Age":18,"Name":"dj"}
Metadata
解码时会产生一些有用的信息,mapstructure可以使用Metadata收集这些信息。Metadata结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
// Metadata 包含关于解码结构的信息,这些信息通常通过其他方式获取起来会比较繁琐或困难。
type Metadata struct {
// Keys 是成功解码的结构的键
Keys []string
// Unused 是一个键的切片,在原始值中被找到,但由于在结果接口中没有匹配的字段,所以未被解码
Unused []string
// Unset 是一个字段名称的切片,在结果接口中被找到,
// 但在解码过程中未被设置,因为在输入中没有匹配的值
Unset []string
}
Metadata只有3个导出字段:
Keys:解码成功的键名;Unused:在源数据中存在,但是目标结构中不存在的键名。Unset:在目标结构中存在,但是源数据中不存在。
使用DecodeMetadata方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
type Person struct {
Name string
Age int
Sex bool
}
func main() {
m := map[string]interface{}{
"name": "dj",
"age": 18,
"job": "programmer",
}
var p Person
//定义一个Metadata结构
var metadata mapstructure.Metadata
//传入DecodeMetadata收集解码的信息
mapstructure.DecodeMetadata(m, &p, &metadata)
fmt.Printf("成功解码的结构的键keys:%#v 源数据未被解码unused:%#v, 结果接口中被找到unset: %#v \n", metadata.Keys, metadata.Unused, metadata.Unset)
}
结果
1
成功解码的结构的键keys:[]string{"Name", "Age"} 源数据未被解码unused:[]string{"job"}, 结果接口中被找到unset: []string{"Sex"}
字段类型错误处理
mapstructure执行转换的过程中不可避免地会产生错误,例如 JSON 中某个键的类型与对应 Go 结构体中的字段类型不一致。Decode/DecodeMetadata会返回这些错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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类型。
结果:
1
2
3
4
5
6
7
8
$ 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方法,它们会尝试做类型自动转换。
注意: 如果类型转换失败了,WeakDecode同样会返回错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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())
}
}
解码器
mapstructure还提供了更灵活的解码器(Decoder)。可以通过配置DecoderConfig实现上面介绍的任何功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// DecoderConfig 是用于创建新解码器的配置,允许自定义解码的各个方面。
type DecoderConfig struct {
// DecodeHook,如果设置了,将在任何解码和任何类型转换(如果 WeaklyTypedInput 打开)之前调用。
// 这允许你在将值设置到结果结构之前修改它们的值。
// DecodeHook 会为输入中的每个映射和值调用一次。这意味着如果结构体具有带有 squash 标签的嵌入字段,
// 解码钩子只会一次使用所有输入数据进行调用,而不是为每个嵌入的结构体分别调用。
//
// 如果返回错误,整个解码将以该错误失败。
DecodeHook DecodeHookFunc
// 如果 ErrorUnused 为 true,则表示在解码过程中存在于原始映射中但未被使用的键是错误的(多余的键)。
ErrorUnused bool
// 如果 ErrorUnset 为 true,则表示在解码过程中存在于结果中但未被设置的字段是错误的(多余的字段)。
// 这仅适用于解码为结构体。这还将影响所有嵌套结构体。
ErrorUnset bool
// ZeroFields,如果设置为 true,在写入字段之前将字段清零。
// 例如,一个映射在放入解码值之前将被清空。如果为 false,映射将会被合并。
ZeroFields bool
// 如果 WeaklyTypedInput 为 true,则解码器将进行以下“弱”转换:
//
// - 布尔值转换为字符串(true = "1",false = "0")
// - 数字转换为字符串(十进制)
// - 布尔值转换为 int/uint(true = 1,false = 0)
// - 字符串转换为 int/uint(基数由前缀隐含)
// - int 转换为布尔值(如果值 != 0 则为 true)
// - 字符串转换为布尔值(接受:1、t、T、TRUE、true、True、0、f、F、
// FALSE、false、False。其他任何值都是错误的)
// - 空数组 = 空映射,反之亦然
// - 负数转换为溢出的 uint 值(十进制)
// - 映射的切片转换为合并的映射
// - 单个值根据需要转换为切片。每个元素都会被弱解码。
// 例如:"4" 如果目标类型是 int 切片,则可以变为 []int{4}。
//
WeaklyTypedInput bool
// Squash 将压缩(squash)嵌入的结构体。也可以通过使用标签将 squash 标签添加到单个结构体字段中。例如:
//
// type Parent struct {
// Child `mapstructure:",squash"`
// }
Squash bool
// Metadata 是将包含有关解码的额外元数据的结构。
// 如果为 nil,则不会跟踪任何元数据。
Metadata *Metadata
// Result 是指向将包含解码值的结构体的指针。
Result interface{}
// 用于字段名称的标签名称,mapstructure 会读取它。默认为 "mapstructure"。
TagName string
// IgnoreUntaggedFields 忽略所有没有明确 TagName 的结构字段,类似于默认行为下的 `mapstructure:"-"`。
IgnoreUntaggedFields bool
// MatchName 是用于匹配映射键与结构体字段名或标签的函数。
// 默认为 `strings.EqualFold`。可以用来实现区分大小写的标签值、支持蛇形命名等。
MatchName func(mapKey, fieldName string) bool
}
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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())
}
}