官网链接: 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

github.com/mitchellh/mapstructure 教程的更多相关文章

  1. github桌面软件使用教程

    github桌面软件使用教程 首先 要先安装 桌面版官网,或者百度搜github windows下载即可 可以再github网站上直接点击,把代码添加的桌面软件中 也可以再左上角添加项目,比如actu ...

  2. git和github新手安装使用教程(三步入门)

    git和github新手安装使用教程(三步入门) 对于新手来说,每次更换设备时,github的安装和配置都会耗费大量时间.主要原因是每次安装时都只关心了[怎么做],而忘记了记住[为什么].本文从操作的 ...

  3. GitHub官方Markdown语法教程

    说明:Markdown随着编译器不一样,语法也都不一样,但这份GitHub提供的官方教程,基本学会这份就够了. https://guides.github.com/features/mastering ...

  4. git与github的简单使用教程

    git与github的简单使用教程 一.创建仓库 点击new,进入创建仓库页面 对将要创建的仓库进行一些简单的设置 最后再点击create repository就可以了. 到这我们就创建好了一个仓库. ...

  5. Github 第三方授权登录教程

    Github 第三方授权登录教程 ####大致流程图 ####1.首先注册一个github帐号,Applications>Developer applications>Register a ...

  6. GitHub和SourceTree入门教程

    -->本教程适用于主流的开源网站github和bitbucket,个人认为sourceTree还是比较好用的git客户端,支持windows和mac os. -->soureceTree的 ...

  7. [转] GitHub上README.md教程

    点击阅读原文 最近对它的README.md文件颇为感兴趣.便写下这贴,帮助更多的还不会编写README文件的同学们. README文件后缀名为md.md是markdown的缩写,markdown是一种 ...

  8. GitHub和SourceTree入门教程——(转载),希望能帮到有需要的人

    -->本教程适用于主流的开源网站github和bitbucket,个人认为sourceTree还是比较好用的git客户端,支持windows和mac os. -->soureceTree的 ...

  9. GitHub上README.md教程

    最近对它的README.md文件颇为感兴趣.便写下这贴,帮助更多的还不会编写README文件的同学们. README文件后缀名为md.md是markdown的缩写,markdown是一种编辑博客的语言 ...

  10. 如何搭建一个独立博客——简明Github Pages与Hexo教程

    摘要:这是一篇很详尽的独立博客搭建教程,里面介绍了域名注册.DNS设置.github和Hexo设置等过程,这是我写得最长的一篇教程.我想将我搭建独立博客的过程在一篇文章中尽可能详细地写出来,希望能给后 ...

随机推荐

  1. vue中render函数使用attrs绑定id、class、style、事件(5)

    1.h函数的三个参数 第一个参数是必须的. 类型:{String | Object | Function} 一个 HTML 标签名.一个组件.一个异步组件.或一个函数式组件. 是要渲染的html标签. ...

  2. 大数据面试题集锦-Hadoop面试题(二)-HDFS

    你准备好面试了吗?这里有一些面试中可能会问到的问题以及相对应的答案.如果你需要更多的面试经验和面试题,关注一下"张飞的猪大数据分享"吧,公众号会不定时的分享相关的知识和资料. 目录 ...

  3. 【二】MADDPG多智能体算法实现(parl)【追逐游戏复现】

    相关文章: [一]MADDPG-单智能体|多智能体总结(理论.算法) [二]MADDPG多智能体深度强化学习算法算法实现(parl)--[追逐游戏复现] 程序链接:直接fork:MADDPG多智能体深 ...

  4. 探索C语言中的Shellcode从提取到执行

    ShellCode是一种独立于应用程序的机器代码,通常用于实现特定任务,如执行远程命令.注入恶意软件或利用系统漏洞.在网络安全领域,研究Shellcode是理解恶意软件和提高系统安全性的关键一环.本文 ...

  5. 3.1 C/C++ 使用字符与指针

    C/C++语言是一种通用的编程语言,具有高效.灵活和可移植等特点.C语言主要用于系统编程,如操作系统.编译器.数据库等:C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统.图形用户界面 ...

  6. DC-9渗透学习

    开靶机,net模式,启动 arp-scan -l命令扫描存活主机 nmap -sS -sV -A -n 192.168.100.22 ┌──(root㉿kali)-[~] └─# nmap -sS - ...

  7. 关于debug一晚上的一些思考,做开发到底要养成什么习惯?

    总结:日志一定要写,日志一定要写,日志一定要写! 今天晚上是我学开发过程中很不一样的一晚,今晚学到了很多. 虽然我也只是一个开发的初学小白,今天的debug分享是我的一个小方法和一个小记录,如果大佬们 ...

  8. 零基础入门学习Java课堂笔记 ——day02

    流程控制 1.Scanner对象 来源:java.util.Scanner 我们可以通过Scanner类来获取用户的输入输出 用户交互Scanner 通过new关键词创建Scanner对象 Scann ...

  9. RHEL7安装11204 RAC的注意事项

    最近在某客户的RHEL7 + 11204 RAC环境上测试遇到不少的坑,好在都赶在正式上线前及时发现并处理完毕. 其中两个问题比较典型所以特别记录下:问题都和主机重启后,O相关服务没有自启动导致,看来 ...

  10. 探索Web API SpeechSynthesis:给你的网页增添声音

    Web API SpeechSynthesis是一项强大的浏览器功能,它允许开发者将文本转换为语音,并通过浏览器播放出来.本文将深入探讨SpeechSynthesis的控制接口,包括其功能.用法和一个 ...