文章

字典解析库

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}

我们定义了两个结构体PersonCat,他们的字段有些许不同。现在,我们约定通信的 JSON 串中有一个type字段。当type的值为person时,该 JSON 串表示的是Person类型的数据。当type的值为cat时,该 JSON 串表示的是Cat类型的数据。

上面代码中,我们先用json.Unmarshal将字节流解码为map[string]interface{}类型。然后读取里面的type字段。根据type字段的值,再使用mapstructure.Decode将该 JSON 串分别解码为PersonCat类型的值,并输出。

实际上,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"PersonName字段映射为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.

注意对比Friend1Friend2使用的 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}} 

注意对比Friend1Friend2使用的 JSON 串的不同。

另外需要注意一点,如果父结构体中有同名的字段,那么mapstructure会将JSON 中对应的值同时设置到这两个字段中,即这两个字段有相同的值。

Remainder Values (未映射的值)

如果源数据中有未映射的值(即结构体中无对应的字段),mapstructure默认会忽略它。

解决方法:

  1. 可以通过在 DecoderConfig 中设置 ErrorUnused 来引发错误。如果正在使用元数据(Metadata),还可以维护一个未使用键的切片(slice)。
  2. 可以在结构体中定义一个字段,为其设置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中字段Namestring类型,但输入中nameint类型;字段Ageint类型,但输入中agestring类型;字段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())
  }
}

© 2024- lfj