github.com/mitchellh/mapstructure 教程

官网链接: github.com/mitchellh/mapstructure

本文只是简单的记录下 mapstructure 库的简单使用,想更加详细的学习,点击 Godoc 学习吧。

文中内容基本都是来自后面的参考链接。

github.com/mitchellh/mapstructure 是一个用于将通用的map值解码为结构体(struct)并进行错误处理的Go库。当你从某个数据流(如JSON、Gob等)中解码值时,这个库非常有用,因为在读取部分数据之前,你可能不知道底层数据的结构。因此,你可以读取一个map[string]interface{} 并使用这个库将其解码为适当的本地Go结构体。

1、基础使用

安装方式:

go get github.com/mitchellh/mapstructure@v1.5.0

在日常开发中,我们接受的数据可能不是固定的格式,而是会根据某个值的不同有不同的内容。我们来一起看一个例子,更加形象的了解这个库的基础使用。

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)
		}
	}
}

运行结果:

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也可以用于网络消息解码,如果你不考虑性能的话

这个例子中,我们可以感受到 mapstructure 库的魅力所在,接下来,我们一起深入的学习如何使用它吧。

2、详细学习

2.1、Field Tags (字段标签)

在解码为结构体时,mapstructure 默认会使用字段名进行映射。例如,如果一个结构体有一个字段名为 "Username",那么 mapstructure 会在源值中查找键 "username"(不区分大小写)。

type User struct {
    Username string
}

通过使用结构体标签来改变 mapstructure 的行为。mapstructure 默认查找的结构体标签是 "mapstructure",但你可以使用 DecoderConfig 进行自定义设置。

这里一定要注意的是:mapstructure 在字段映射的时候是 case insensitive,即大小写不敏感的。

2.2、Renaming Fields

在实际使用过程中,我们可能需要重命名 mapstructure 查找的键,这个时候,可以使用 "mapstructure" 标签并直接设置一个值。例如,要将上面的 "username" 示例更改为 "user":

type User struct {
    Username string `mapstructure:"user"`
}

2.3、Embedded Structs and Squashing(内嵌结构)

结构体可以任意嵌套,嵌套的结构被认为是拥有该结构体名字的另一个字段。例如,下面两种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",
}

例子1

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)
		}
	}
}

结果:

friend1 {{dj}}
friend2 {{dj2}}
Exiting.

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

接着看这个例子2

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)
		}
	}
}

结果:

friend1: {Type:friend1 Person:{Name:dj Type:}} 
friend2: {Type:friend2 Person:{Name:dj2 Type:friend2}} 

例子1和例子2 的区别在于,例子2 中父结构和子结构体中有相同的字段,这个时候,如果为子结构体定义了mapstructure:",squash" 的话,那么mapstructure会将JSON 中对应的值同时设置到这两个字段中,即这两个字段有相同的值。

其实这里也跟使用的 JSON字符串的值有关,大家可以自行尝试下,不确定的时候,先写个 demo 看看。

2.4、Remainder Values (未映射的值)

如果在源值中存在任何未映射的键,默认情况下,mapstructure 将会静默地忽略它们(即结构体中无对应的字段)。

你可以通过在 DecoderConfig 中设置 ErrorUnused 来引发错误。如果你正在使用元数据(Metadata),还可以维护一个未使用键的切片(slice)。

你还可以在标签上使用 ",remain" 后缀,将所有未使用的值收集到一个映射(map)中。带有这个标签的字段必须是一个映射类型,只能是 "map[string]interface{}" 或 "map[interface{}]interface{}" 这两种类型之一。请参阅下面的示例:

type Friend struct {
    Name  string
    Other map[string]interface{} `mapstructure:",remain"`
}

加入给定下面的输入,"Other" 字段将会被填充为未使用的其他值(除了 "name" 之外的所有值):

map[string]interface{}{
    "name":    "bob",
    "address": "123 Maple St.",
}

完整例子:

package main

import (
	"fmt"
	"github.com/mitchellh/mapstructure"
)

type Friend struct {
	Name  string
	Other map[string]interface{} `mapstructure:",remain"`
}

func main() {
	m := map[string]interface{}{
		"name":    "bob",
		"address": "123 Maple St.",
	}

	var f Friend
	err := mapstructure.Decode(m, &f)
	fmt.Println("err->", err)
	fmt.Printf("friend: %+v", f)
}


结果:

err-> <nil>
friend: {Name:bob Other:map[address:123 Maple St.]}

2.5、Omit Empty Values(忽略空值)

我们在使用 json 库时,对于空值我们不需要展示的时候,可以使用 "json:,omitempty" 来忽略。 mapstructure 也是一样的。

当从结构体解码到其他任何值时,你可以在标签上使用 ",omitempty" 后缀,以便在该值等于零值时省略它。所有类型的零值在 Go 规范中有明确定义。

例如,数值类型的零值是零("0")。如果结构体字段的值为零且是数值类型,该字段将为空,且不会被编码到目标类型中。

type Source struct {
    Age int `mapstructure:",omitempty"`
}

2.6、Unexported fields

Go 中规定了 未导出的(私有的)结构体字段不能在定义它们的包之外进行设置,解码器将直接跳过它们。

通过以下例子来进行讲解:

package main

import (
	"fmt"
	"github.com/mitchellh/mapstructure"
)

type Exported struct {
	private string // this unexported field will be skipped
	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)
}


// 输出
e: {private: Public:I made it through!}

2.7、Other Configuration

mapstructure是高度可配置的。有关支持的其他功能和选项,请参阅 DecoderConfig 结构。

2.8、逆向转换

前面我们都是将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",且对象pJob字段未设置。运行结果:

$ go run main.go 
{"Age":18,"Name":"dj"}

2.9、Metadata

解码时会产生一些有用的信息,mapstructure可以使用Metadata收集这些信息。Metadata结构如下:

// Metadata 包含关于解码结构的信息,这些信息通常通过其他方式获取起来会比较繁琐或困难。
type Metadata struct {
	// Keys 是成功解码的结构的键
	Keys []string

	// Unused 是一个键的切片,在原始值中被找到,但由于在结果接口中没有匹配的字段,所以未被解码
	Unused []string

	// Unset 是一个字段名称的切片,在结果接口中被找到,
	// 但在解码过程中未被设置,因为在输入中没有匹配的值
	Unset []string
}

Metadata只有3个导出字段:

  • Keys:解码成功的键名;
  • Unused:在源数据中存在,但是目标结构中不存在的键名。
  • Unset:在目标结构中存在,但是源数据中不存在。

为了收集这些数据,我们需要使用DecodeMetadata来代替Decode方法:

接下来我们一起看个例子来进行学习:

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
	var metadata mapstructure.Metadata
	mapstructure.DecodeMetadata(m, &p, &metadata)

	fmt.Printf("keys:%#v unused:%#v, unset: %#v \n", metadata.Keys, metadata.Unused, metadata.Unset)
}


// 结果
keys:[]string{"Name", "Age"} unused:[]string{"job"}, unset: []string{"Sex"} 

2.10、错误处理

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中字段Namestring类型,但输入中nameint类型;字段Ageint类型,但输入中agestring类型;字段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'

从错误信息中很容易看出哪里出错了。

2.11、弱类型输入

有时候,我们并不想对结构体字段类型和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对应的值123int类型,但是在WeakDecode中会将其转换为string类型以匹配Person.Name字段的类型。同样的,age的值"18"string类型,在WeakDecode中会将其转换为int类型以匹配Person.Age字段的类型。 需要注意一点,如果类型转换失败了,WeakDecode同样会返回错误。例如将上例中的age设置为"bad value",它就不能转为int类型,故而返回错误。

2.12、解码器

除了上面介绍的方法外,mapstructure还提供了更灵活的解码器(Decoder)。可以通过配置DecoderConfig实现上面介绍的任何功能:

// 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
}

例子:

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方法实现的。

参考链接:

Godoc

Go 每日一库之 mapstructure

posted @ 2023-08-27 22:28  画个一样的我  阅读(845)  评论(0编辑  收藏  举报