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}
我们定义了两个结构体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 库的魅力所在,接下来,我们一起深入的学习如何使用它吧。
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.
注意对比Friend1
和Friend2
使用的 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"
,且对象p
的Job
字段未设置。运行结果:
$ 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
中字段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'
从错误信息中很容易看出哪里出错了。
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
对应的值123
是int
类型,但是在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
方法实现的。