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方法实现的。
参考链接:
github.com/mitchellh/mapstructure 教程的更多相关文章
- github桌面软件使用教程
github桌面软件使用教程 首先 要先安装 桌面版官网,或者百度搜github windows下载即可 可以再github网站上直接点击,把代码添加的桌面软件中 也可以再左上角添加项目,比如actu ...
- git和github新手安装使用教程(三步入门)
git和github新手安装使用教程(三步入门) 对于新手来说,每次更换设备时,github的安装和配置都会耗费大量时间.主要原因是每次安装时都只关心了[怎么做],而忘记了记住[为什么].本文从操作的 ...
- GitHub官方Markdown语法教程
说明:Markdown随着编译器不一样,语法也都不一样,但这份GitHub提供的官方教程,基本学会这份就够了. https://guides.github.com/features/mastering ...
- git与github的简单使用教程
git与github的简单使用教程 一.创建仓库 点击new,进入创建仓库页面 对将要创建的仓库进行一些简单的设置 最后再点击create repository就可以了. 到这我们就创建好了一个仓库. ...
- Github 第三方授权登录教程
Github 第三方授权登录教程 ####大致流程图 ####1.首先注册一个github帐号,Applications>Developer applications>Register a ...
- GitHub和SourceTree入门教程
-->本教程适用于主流的开源网站github和bitbucket,个人认为sourceTree还是比较好用的git客户端,支持windows和mac os. -->soureceTree的 ...
- [转] GitHub上README.md教程
点击阅读原文 最近对它的README.md文件颇为感兴趣.便写下这贴,帮助更多的还不会编写README文件的同学们. README文件后缀名为md.md是markdown的缩写,markdown是一种 ...
- GitHub和SourceTree入门教程——(转载),希望能帮到有需要的人
-->本教程适用于主流的开源网站github和bitbucket,个人认为sourceTree还是比较好用的git客户端,支持windows和mac os. -->soureceTree的 ...
- GitHub上README.md教程
最近对它的README.md文件颇为感兴趣.便写下这贴,帮助更多的还不会编写README文件的同学们. README文件后缀名为md.md是markdown的缩写,markdown是一种编辑博客的语言 ...
- 如何搭建一个独立博客——简明Github Pages与Hexo教程
摘要:这是一篇很详尽的独立博客搭建教程,里面介绍了域名注册.DNS设置.github和Hexo设置等过程,这是我写得最长的一篇教程.我想将我搭建独立博客的过程在一篇文章中尽可能详细地写出来,希望能给后 ...
随机推荐
- CentOS确认网口是否插入网线的办法
最近公司的机器存在网络问题, 部分网络总是不通, 比较奇怪. 最近一直想处理好. 第一步: 先查看网口的设备信息 可以使用 ip link show 可以讲网口信息都展示出来. 一般情况下 NO-C ...
- 【K哥爬虫普法】大数据风控第一案:从魔蝎科技案件判决,看爬虫技术刑事边界
我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...
- Jupyter Notebook支持Go
在执行下列命令之前,请确保你已经安装了Go和Jupyter. gophernotes是针对Jupyter和nteract的Go内核,它可以让你在基于浏览器的笔记本或桌面app上交互式地使用Go.下面介 ...
- C++ 实现的Ping类的封装
Ping 使用 Internet 控制消息协议(ICMP)来测试主机之间的连接.当用户发送一个 ping 请求时,则对应的发送一个 ICMP Echo 请求消息到目标主机,并等待目标主机回复一个 IC ...
- 关于TypeScript中提示xxx is declared but its value is never read的解决方法
首先,提示很明显,是定义了变量,但是却没有使用.解决方案有如下两种: 一: 需要确定变量是否真的没有使用到,如果没有使用直接删除即可. 二: 对于方法中的入参,是没法随便删除的.这时候我们可以利用Ty ...
- elasticsearch源码debug
一.下载源代码 直接用idea下载代码https://github.com/elastic/elasticsearch.git 切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Deb ...
- linux 后台运行进程:& , nohup
目录 后台执行 & nohup 查看后台运行的命令 jobs ps 关闭当前后台运行的程序 kill 前后台进程的切换与控制 ctrl + z 命令 fg 命令 bg 命令 思考 问题1-为什 ...
- 【Unity3D】人物跟随鼠标位置
1 游戏对象 2D动画和人体模型及动画中介绍了 Aniamtion.Animator.人体模型.人体骨骼.人体动画等基础知识,本文将通过 "人物跟随鼠标位置" 案例加强对 Un ...
- 数据抽取平台pydatax介绍
缘起一: 公司现有数据仓库,是通过kettle从mysql抽取到目标库,运行多年,主要有以下问题, 1,效率低:kettle抽取行数少 2,容错性差:一个表抽取出错就导致后续计算 ...
- mysql插入表中的中文字符显示为乱码或问号的解决方法
mysql中文显示乱码或者问号是因为选用的编码不对或者编码不一致造成的,最简单的方法就是修改mysql的配置文件my.cnf.在[mydqld]和[client]段加入 default-charact ...