使用 mapstructure 解析 json

介绍

先来介绍一下 mapstructure 这个库主要用来做什么的吧,官网是这么介绍的:

mapstructure 是一个 Go 库,用于将通用映射值解码为结构,反之亦然,同时提供有用的错误处理。

该库在解码数据流(JSON、Gob 等)中的值时最为有用,因为在读取部分数据之前,您并不十分清楚底层数据的结构。因此,您可以读取 map[string]interface{} 并使用此库将其解码为适当的本地 Go 底层结构。

简单来说,它擅长解析一些我们并不十分清楚底层数据结构的数据流到我们定义的结构体中。

下面我们通过几个例子来简单介绍一下 mapstructure 怎么使用。

例子

普通形式

go 复制代码
func normalDecode() {
	type Person struct {
		Name   string
		Age    int
		Emails []string
		Extra  map[string]string
	}

	// 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
	input := map[string]interface{}{
		"name":   "Tim",
		"age":    31,
		"emails": []string{"one@gmail.com", "two@gmail.com", "three@gmail.com"},
		"extra": map[string]string{
			"twitter": "Tim",
		},
	}

	var result Person
	err := mapstructure.Decode(input, &result)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%#v\n", result)
}

输出:

go 复制代码
main.Person{Name:"Tim", Age:31, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Tim"}}

这个方式应该是我们最经常使用的,非常简单的将 map[string]interface{} 映射到我们的结构体中。

在这里,我们并没有指定每个 field 的 tag,让 mapstructure 自动去映射。

如果我们的 input 是一个 json 字符串,那么我们需要将 json 字符串解析为 map[string]interface{} 之后,再将其映射到我们的结构体中。

go 复制代码
func jsonDecode() {
	var jsonStr = `{
	"name": "Tim",
	"age": 31,
	"gender": "male"
}`

	type Person struct {
		Name   string
		Age    int
		Gender string
	}
	m := make(map[string]interface{})
	err := json.Unmarshal([]byte(jsonStr), &m)
	if err != nil {
		panic(err)
	}

	var result Person
	err = mapstructure.Decode(m, &result)
	if err != nil {
		panic(err.Error())
	}
	fmt.Printf("%#v\n", result)
}

输出:

go 复制代码
main.Person{Name:"Tim", Age:31, Gender:"male"}

嵌入式结构

mapstructure 允许我们压缩多个嵌入式结构,并通过 squash 标签进行处理。

go 复制代码
func embeddedStructDecode() {
	// 使用 squash 标签允许压缩多个嵌入式结构。通过创建多种类型的复合结构并对其进行解码来演示此功能。
	type Family struct {
		LastName string
	}
	type Location struct {
		City string
	}
	type Person struct {
		Family    `mapstructure:",squash"`
		Location  `mapstructure:",squash"`
		FirstName string
	}

	input := map[string]interface{}{
		"FirstName": "Tim",
		"LastName":  "Liu",
		"City":      "China, Guangdong",
	}

	var result Person
	err := mapstructure.Decode(input, &result)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s %s, %s\n", result.FirstName, result.LastName, result.City)
}

输出:

go 复制代码
Tim Liu, China, Guangdong

在这个例子中, Person 里面有着 Location 和 Family 的嵌入式结构体,通过 squash 标签进行压缩,从而达到平铺的作用。

元数据

go 复制代码
func metadataDecode() {
	type Person struct {
		Name   string
		Age    int
		Gender string
	}

	// 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
	input := map[string]interface{}{
		"name":  "Tim",
		"age":   31,
		"email": "one@gmail.com",
	}

	// 对于元数据,我们制作了一个更高级的 DecoderConfig,以便我们可以更细致地配置所使用的解码器。在这种情况下,我们只是告诉解码器我们想要跟踪元数据。
	var md mapstructure.Metadata
	var result Person
	config := &mapstructure.DecoderConfig{
		Metadata: &md,
		Result:   &result,
	}

	decoder, err := mapstructure.NewDecoder(config)
	if err != nil {
		panic(err)
	}

	if err = decoder.Decode(input); err != nil {
		panic(err)
	}

	fmt.Printf("value: %#v, keys: %#v, Unused keys: %#v, Unset keys: %#v\n", result, md.Keys, md.Unused, md.Unset)
}

输出:

go 复制代码
value: main.Person{Name:"Tim", Age:31, Gender:""}, keys: []string{"Name", "Age"}, Unused keys: []string{"email"}, Unset keys: []string{"Gender"}

从这个例子我们可以看出,使用 Metadata 可以记录我们结构体以及 map[string]interface{} 的差异,相同的部分会正确映射到对应的字段中,而差异则使用了 Unused 和 Unset 来表达。

Unused:map 中有着结构体所没有的字段。

Unset:结构体中有着 map 中所没有的字段。

避免空值的映射

这里的使用其实和内置的 json 库使用方式是一样的,都是借助 omitempty 标签来解决。

go 复制代码
func omitemptyDecode() {
	// 添加 omitempty 注释以避免空值的映射键
	type Family struct {
		LastName string
	}
	type Location struct {
		City string
	}
	type Person struct {
		*Family   `mapstructure:",omitempty"`
		*Location `mapstructure:",omitempty"`
		Age       int
		FirstName string
	}

	result := &map[string]interface{}{}
	input := Person{FirstName: "Somebody"}
	err := mapstructure.Decode(input, &result)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", result)
}

输出:

go 复制代码
&map[Age:0 FirstName:Somebody]

这里我们可以看到 *Family 和 *Location 都被设置了 omitempty,所以在解析过程中会忽略掉空值。而 Age 没有设置,并且 input 中没有对应的 value,所以在解析中使用对应类型的零值来表达,而 int 类型的零值就是 0。

剩余字段

go 复制代码
func remainDataDecode() {
	type Person struct {
		Name  string
		Age   int
		Other map[string]interface{} `mapstructure:",remain"`
	}

	input := map[string]interface{}{
		"name":   "Tim",
		"age":    31,
		"email":  "one@gmail.com",
		"gender": "male",
	}

	var result Person
	err := mapstructure.Decode(input, &result)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%#v\n", result)
}

输出:

go 复制代码
main.Person{Name:"Tim", Age:31, Other:map[string]interface {}{"email":"one@gmail.com", "gender":"male"}}

从代码可以看到 Other 字段被设置了 remain,这意味着 input 中没有正确映射的字段都会被放到 Other 中,从输出可以看到,email 和 gender 已经被正确的放到 Other 中了。

自定义标签

go 复制代码
func tagDecode() {
	// 请注意,结构类型中定义的 mapstructure 标签可以指示将值映射到哪些字段。
	type Person struct {
		Name string `mapstructure:"person_name"`
		Age  int    `mapstructure:"person_age"`
	}

	input := map[string]interface{}{
		"person_name": "Tim",
		"person_age":  31,
	}

	var result Person
	err := mapstructure.Decode(input, &result)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%#v\n", result)
}

输出:

go 复制代码
main.Person{Name:"Tim", Age:31}

在 Person 结构中,我们将 person_name 和 person_age 分别映射到 Name 和 Age 中,从而达到在不破坏结构的基础上,去正确的解析。

弱类型解析

正如前面所说,mapstructure 提供了类似 PHP 解析弱类型结构的方法。

go 复制代码
func weaklyTypedInputDecode() {
	type Person struct {
		Name   string
		Age    int
		Emails []string
	}

	// 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,由 PHP 等弱类型语言生成。
	input := map[string]interface{}{
		"name":   123,  // number => string
		"age":    "31", // string => number
		"emails": map[string]interface{}{}, // empty map => empty array
	}

	var result Person
	config := &mapstructure.DecoderConfig{
		WeaklyTypedInput: true,
		Result:           &result,
	}

	decoder, err := mapstructure.NewDecoder(config)
	if err != nil {
		panic(err)
	}

	err = decoder.Decode(input)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%#v\n", result)
}

输出:

go 复制代码
main.Person{Name:"123", Age:31, Emails:[]string{}}

从代码可以看到,input 中的 name、age 和 Person 结构体中的 Name、Age 类型不一致,而 email 更是离谱,一个字符串数组,一个是 map。

但是我们通过自定义 DecoderConfig,将 WeaklyTypedInput 设置成 true 之后,mapstructure 很容易帮助我们解决这类弱类型的解析问题。

但是也不是所有问题都能解决,通过源码我们可以知道有如下限制:

go 复制代码
//   - bools to string (true = "1", false = "0")
//   - numbers to string (base 10)
//   - bools to int/uint (true = 1, false = 0)
//   - strings to int/uint (base implied by prefix)
//   - int to bool (true if value != 0)
//   - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F,
//     FALSE, false, False. Anything else is an error)
//   - empty array = empty map and vice versa
//   - negative numbers to overflowed uint values (base 10)
//   - slice of maps to a merged map
//   - single values are converted to slices if required. Each
//     element is weakly decoded. For example: "4" can become []int{4}
//     if the target type is an int slice.

大家使用这种弱类型解析的时候也需要注意。

错误处理

mapstructure 错误提示非常的友好,下面我们来看看遇到错误时,它是怎么提示的。

go 复制代码
func decodeErrorHandle() {
	type Person struct {
		Name   string
		Age    int
		Emails []string
		Extra  map[string]string
	}

	input := map[string]interface{}{
		"name":   123,
		"age":    "bad value",
		"emails": []int{1, 2, 3},
	}

	var result Person
	err := mapstructure.Decode(input, &result)
	if err != nil {
		fmt.Println(err.Error())
	}
}

输出:

go 复制代码
5 error(s) decoding:

* 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value'
* 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1'
* 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2'
* 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3'
* 'Name' expected type 'string', got unconvertible type 'int', value: '123'

这里的错误提示会告诉我们每个字段,字段里的值应该需要怎么表达,我们可以通过这些错误提示,比较快的去修复问题。

总结

从上面这些例子看看到 mapstructure 的强大之处,很好的帮我们解决了实实在在的问题,也在节省我们的开发成本。

但是从源码来看,内部使用了大量的反射,这可能会对一些特殊场景带来性能隐患。所以大家在使用的时候,一定要充分考虑产品逻辑以及场景。

以下贴一小段删减过的源码:

go 复制代码
// Decode decodes the given raw interface to the target pointer specified
// by the configuration.
func (d *Decoder) Decode(input interface{}) error {
	return d.decode("", input, reflect.ValueOf(d.config.Result).Elem())
}

// Decodes an unknown data type into a specific reflection value.
func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error {
	....

	var err error
	outputKind := getKind(outVal)
	addMetaKey := true
	switch outputKind {
	case reflect.Bool:
		err = d.decodeBool(name, input, outVal)
	case reflect.Interface:
		err = d.decodeBasic(name, input, outVal)
	case reflect.String:
		err = d.decodeString(name, input, outVal)
	case reflect.Int:
		err = d.decodeInt(name, input, outVal)
	case reflect.Uint:
		err = d.decodeUint(name, input, outVal)
	case reflect.Float32:
		err = d.decodeFloat(name, input, outVal)
	case reflect.Struct:
		err = d.decodeStruct(name, input, outVal)
	case reflect.Map:
		err = d.decodeMap(name, input, outVal)
	case reflect.Ptr:
		addMetaKey, err = d.decodePtr(name, input, outVal)
	case reflect.Slice:
		err = d.decodeSlice(name, input, outVal)
	case reflect.Array:
		err = d.decodeArray(name, input, outVal)
	case reflect.Func:
		err = d.decodeFunc(name, input, outVal)
	default:
		// If we reached this point then we weren't able to decode it
		return fmt.Errorf("%s: unsupported type: %s", name, outputKind)
	}

	// If we reached here, then we successfully decoded SOMETHING, so
	// mark the key as used if we're tracking metainput.
	if addMetaKey && d.config.Metadata != nil && name != "" {
		d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
	}

	return err
}
相关推荐
djk888813 小时前
.net6.0(.net Core)读取 appsettings.json 配置文件
json·.net·.netcore
一条晒干的咸魚19 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
黎明晓月2 天前
PostgreSQL提取JSON格式的数据(包含提取list指定索引数据)
postgresql·json·list
心死翼未伤2 天前
python从入门到精通:pyspark实战分析
开发语言·数据结构·python·spark·json
Mephisto.java2 天前
【大数据学习 | flume】flume Sink Processors与拦截器Interceptor
大数据·sql·oracle·sqlite·json·flume
ac-er88883 天前
ThinkPHP中使用ajax接收json数据的方法
前端·ajax·json·php
0x派大星3 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
不能只会打代码3 天前
支持用户注册和登录、发布动态、点赞、评论、私信等功能的社交媒体平台创建!!!
前端·css·后端·html·json·媒体·社交媒体平台
愚公码农4 天前
MySQL json字段索引添加及使用
数据库·mysql·json
拧螺丝专业户4 天前
gin源码阅读(2)请求体中的JSON参数是如何解析的?
前端·json·gin