github.com/mitchellh/mapstructure 教程
<p>官网链接: github.com/mitchellh/mapstructure</p><p>本文只是简单的记录下 mapstructure 库的简单使用,想更加详细的学习,点击 Godoc 学习吧。</p>
<blockquote>
<p>文中内容基本都是来自后面的参考链接。</p>
</blockquote>
<p>github.com/mitchellh/mapstructure是一个用于将通用的map值解码为结构体(struct)并进行错误处理的Go库。当你从某个数据流(如JSON、Gob等)中解码值时,这个库非常有用,因为在读取部分数据之前,你可能不知道底层数据的结构。因此,你可以读取一个<code>mapinterface{} </code>并使用这个库将其解码为适当的本地Go结构体。</p>
<h3 id="1基础使用">1、基础使用</h3>
<p>安装方式:</p>
<pre><code class="language-sh">go get github.com/mitchellh/mapstructure@v1.5.0
</code></pre>
<p>在日常开发中,我们接受的数据可能不是固定的格式,而是会根据某个值的不同有不同的内容。我们来一起看一个例子,更加形象的了解这个库的基础使用。</p>
<pre><code class="language-go">package main
import (
"encoding/json"
"fmt"
"log"
"github.com/mitchellh/mapstructure"
)
type Person struct {
Name string
Ageint
Jobstring
}
type Cat struct {
Namestring
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 mapinterface{}
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)
}
}
}
</code></pre>
<p>运行结果:</p>
<pre><code>person: {dj 18 programmer}
cat: {kitty 1 Ragdoll}
</code></pre>
<p>我们定义了两个结构体<code>Person</code>和<code>Cat</code>,他们的字段有些许不同。现在,我们约定通信的 JSON 串中有一个<code>type</code>字段。当<code>type</code>的值为<code>person</code>时,该 JSON 串表示的是<code>Person</code>类型的数据。当<code>type</code>的值为<code>cat</code>时,该 JSON 串表示的是<code>Cat</code>类型的数据。</p>
<p>上面代码中,我们先用<code>json.Unmarshal</code>将字节流解码为<code>mapinterface{}</code>类型。然后读取里面的<code>type</code>字段。根据<code>type</code>字段的值,再使用<code>mapstructure.Decode</code>将该 JSON 串分别解码为<code>Person</code>和<code>Cat</code>类型的值,并输出。</p>
<p>实际上,Google Protobuf 通常也使用这种方式。在协议中添加消息 ID 或<strong>全限定消息名</strong>。接收方收到数据后,先读取协议 ID 或<strong>全限定消息名</strong>。然后调用 Protobuf 的解码方法将其解码为对应的<code>Message</code>结构。从这个角度来看,<code>mapstructure</code>也可以用于网络消息解码,<strong>如果你不考虑性能的话</strong>。</p>
<p>这个例子中,我们可以感受到 mapstructure 库的魅力所在,接下来,我们一起深入的学习如何使用它吧。</p>
<h3 id="2详细学习">2、详细学习</h3>
<h4 id="21field-tags-字段标签">2.1、Field Tags (字段标签)</h4>
<p>在解码为结构体时,<code>mapstructure</code> 默认会使用字段名进行映射。例如,如果一个结构体有一个字段名为 "Username",那么 <code>mapstructure</code> 会在源值中查找键 "username"(<strong>不区分大小写</strong>)。</p>
<pre><code class="language-go">type User struct {
Username string
}
</code></pre>
<p>通过使用结构体标签来改变 <code>mapstructure</code> 的行为。<code>mapstructure</code> 默认查找的结构体标签是 "mapstructure",但你可以使用 <code>DecoderConfig</code> 进行自定义设置。</p>
<blockquote>
<p>这里一定要注意的是:<code>mapstructure 在字段映射的时候是 case insensitive,即大小写不敏感的。</code></p>
</blockquote>
<h4 id="22renaming-fields">2.2、Renaming Fields</h4>
<p>在实际使用过程中,我们可能需要重命名 <code>mapstructure</code> 查找的键,这个时候,可以使用 "mapstructure" 标签并直接设置一个值。例如,要将上面的 "username" 示例更改为 "user":</p>
<pre><code class="language-go">type User struct {
Username string `mapstructure:"user"`
}
</code></pre>
<h4 id="23embedded-structs-and-squashing内嵌结构">2.3、Embedded Structs and Squashing(内嵌结构)</h4>
<p>结构体可以任意嵌套,嵌套的结构被认为是拥有该结构体名字的另一个字段。例如,下面两种<code>Friend</code>的定义方式对于<code>mapstructure</code>是一样的:</p>
<pre><code class="language-go">type Person struct {
Name string
}
// 方式一
type Friend struct {
Person
}
// 方式二
type Friend struct {
Person Person
}
</code></pre>
<p>为了正确解码,<code>Person</code>结构的数据要在<code>person</code>键下:</p>
<pre><code class="language-go">mapinterface{} {
"person": mapinterface{}{"name": "dj"},
}
</code></pre>
<p>我们也可以设置<code>mapstructure:",squash"</code>将该结构体的字段提到父结构中:</p>
<pre><code class="language-go">type Friend struct {
Person `mapstructure:",squash"`
}
</code></pre>
<p>这样只需要这样的 JSON 串,无效嵌套<code>person</code>键:</p>
<pre><code class="language-go">mapinterface{}{
"name": "dj",
}
</code></pre>
<p>看<strong>例子1</strong>:</p>
<pre><code class="language-go">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 mapinterface{}
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)
}
}
}
</code></pre>
<p>结果:</p>
<pre><code>friend1 {{dj}}
friend2 {{dj2}}
Exiting.
</code></pre>
<p>注意对比<code>Friend1</code>和<code>Friend2</code>使用的 JSON 串的不同。</p>
<p>接着看这个<strong>例子2</strong>:</p>
<pre><code class="language-go">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 mapinterface{}
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)
}
}
}
</code></pre>
<p>结果:</p>
<pre><code>friend1: {Type:friend1 Person:{Name:dj Type:}}
friend2: {Type:friend2 Person:{Name:dj2 Type:friend2}}
</code></pre>
<p>例子1和例子2 的区别在于,例子2 中父结构和子结构体中有相同的字段,这个时候,如果为子结构体定义了<code>mapstructure:",squash"</code>的话,那么<code>mapstructure</code>会将JSON 中对应的值<strong>同时设置到这两个字段中</strong>,即这两个字段有相同的值。</p>
<blockquote>
<p>其实这里也跟使用的 JSON字符串的值有关,大家可以自行尝试下,不确定的时候,先写个 demo 看看。</p>
</blockquote>
<h4 id="24remainder-values-未映射的值">2.4、Remainder Values (未映射的值)</h4>
<p>如果在源值中存在任何未映射的键,默认情况下,<code>mapstructure</code> 将会静默地忽略它们(即结构体中无对应的字段)。</p>
<p>你可以通过在 <code>DecoderConfig</code> 中设置 <code>ErrorUnused</code> 来引发错误。如果你正在使用元数据(Metadata),还可以维护一个未使用键的切片(slice)。</p>
<p>你还可以在标签上使用 ",remain" 后缀,将所有未使用的值收集到一个映射(map)中。带有这个标签的字段必须是一个映射类型,只能是 <code> "mapinterface{}" 或 "mapinterface{}"</code> 这两种类型之一。请参阅下面的示例:</p>
<pre><code class="language-go">type Friend struct {
Namestring
Other mapinterface{} `mapstructure:",remain"`
}
</code></pre>
<p>加入给定下面的输入,"Other" 字段将会被填充为未使用的其他值(除了 "name" 之外的所有值):</p>
<pre><code class="language-go">mapinterface{}{
"name": "bob",
"address": "123 Maple St.",
}
</code></pre>
<p>完整例子:</p>
<pre><code class="language-go">package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
type Friend struct {
Namestring
Other mapinterface{} `mapstructure:",remain"`
}
func main() {
m := mapinterface{}{
"name": "bob",
"address": "123 Maple St.",
}
var f Friend
err := mapstructure.Decode(m, &f)
fmt.Println("err->", err)
fmt.Printf("friend: %+v", f)
}
</code></pre>
<p>结果:</p>
<pre><code>err-> <nil>
friend: {Name:bob Other:map}
</code></pre>
<h4 id="25omit-empty-values忽略空值">2.5、Omit Empty Values(忽略空值)</h4>
<p>我们在使用 json 库时,对于空值我们不需要展示的时候,可以使用 <code>"json:,omitempty" </code> 来忽略。 mapstructure 也是一样的。</p>
<p>当从结构体解码到其他任何值时,你可以在标签上使用 ",omitempty" 后缀,以便在该值等于零值时省略它。所有类型的零值在 Go 规范中有明确定义。</p>
<p>例如,数值类型的零值是零("0")。如果结构体字段的值为零且是数值类型,该字段将为空,且不会被编码到目标类型中。</p>
<pre><code class="language-go">type Source struct {
Age int `mapstructure:",omitempty"`
}
</code></pre>
<h4 id="26unexported-fields">2.6、Unexported fields</h4>
<p>Go 中规定了 未导出的(私有的)结构体字段不能在定义它们的包之外进行设置,解码器将直接跳过它们。</p>
<p>通过以下例子来进行讲解:</p>
<pre><code class="language-go">package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
type Exported struct {
private string // this unexported field will be skipped
Publicstring
}
func main() {
m := mapinterface{}{
"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!}
</code></pre>
<h4 id="27other-configuration">2.7、Other Configuration</h4>
<p>mapstructure是高度可配置的。有关支持的其他功能和选项,请参阅 DecoderConfig 结构。</p>
<h4 id="28逆向转换">2.8、逆向转换</h4>
<p>前面我们都是将<code>mapinterface{}</code>解码到 Go 结构体中。<code>mapstructure</code>当然也可以将 Go 结构体反向解码为<code>mapinterface{}</code>。在反向解码时,我们可以为某些字段设置<code>mapstructure:",omitempty"</code>。这样当这些字段为默认值时,就不会出现在结构的<code>mapinterface{}</code>中:</p>
<pre><code class="language-go">type Person struct {
Name string
Ageint
Jobstring `mapstructure:",omitempty"`
}
func main() {
p := &Person{
Name: "dj",
Age:18,
}
var m mapinterface{}
mapstructure.Decode(p, &m)
data, _ := json.Marshal(m)
fmt.Println(string(data))
}
</code></pre>
<p>上面代码中,我们为<code>Job</code>字段设置了<code>mapstructure:",omitempty"</code>,且对象<code>p</code>的<code>Job</code>字段未设置。运行结果:</p>
<pre><code>$ go run main.go
{"Age":18,"Name":"dj"}
</code></pre>
<h4 id="29metadata">2.9、Metadata</h4>
<p>解码时会产生一些有用的信息,<code>mapstructure</code>可以使用<code>Metadata</code>收集这些信息。<code>Metadata</code>结构如下:</p>
<pre><code class="language-go">// Metadata 包含关于解码结构的信息,这些信息通常通过其他方式获取起来会比较繁琐或困难。
type Metadata struct {
// Keys 是成功解码的结构的键
Keys []string
// Unused 是一个键的切片,在原始值中被找到,但由于在结果接口中没有匹配的字段,所以未被解码
Unused []string
// Unset 是一个字段名称的切片,在结果接口中被找到,
// 但在解码过程中未被设置,因为在输入中没有匹配的值
Unset []string
}
</code></pre>
<p><code>Metadata</code>只有3个导出字段:</p>
<ul>
<li><code>Keys</code>:解码成功的键名;</li>
<li><code>Unused</code>:在源数据中存在,但是目标结构中不存在的键名。</li>
<li><code>Unset</code>:在目标结构中存在,但是源数据中不存在。</li>
</ul>
<p>为了收集这些数据,我们需要使用<code>DecodeMetadata</code>来代替<code>Decode</code>方法:</p>
<p>接下来我们一起看个例子来进行学习:</p>
<pre><code class="language-go">package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
type Person struct {
Name string
Ageint
Sexbool
}
func main() {
m := mapinterface{}{
"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"}
</code></pre>
<h4 id="210错误处理">2.10、错误处理</h4>
<p><code>mapstructure</code>执行转换的过程中不可避免地会产生错误,例如 JSON 中某个键的类型与对应 Go 结构体中的字段类型不一致。<code>Decode/DecodeMetadata</code>会返回这些错误:</p>
<pre><code class="language-go">type Person struct {
Name string
Age int
Emails []string
}
func main() {
m := mapinterface{}{
"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())
}
}
</code></pre>
<p>上面代码中,结构体中<code>Person</code>中字段<code>Name</code>为<code>string</code>类型,但输入中<code>name</code>为<code>int</code>类型;字段<code>Age</code>为<code>int</code>类型,但输入中<code>age</code>为<code>string</code>类型;字段<code>Emails</code>为<code>[]string</code>类型,但输入中<code>emails</code>为<code>[]int</code>类型。故<code>Decode</code>返回错误。运行结果:</p>
<pre><code class="language-sh">$ go run main.go
5 error(s) decoding:
* 'Age' expected type 'int', got unconvertible type 'string'
* 'Emails' expected type 'string', got unconvertible type 'int'
* 'Emails' expected type 'string', got unconvertible type 'int'
* 'Emails' expected type 'string', got unconvertible type 'int'
* 'Name' expected type 'string', got unconvertible type 'int'
</code></pre>
<p>从错误信息中很容易看出哪里出错了。</p>
<h4 id="211弱类型输入">2.11、弱类型输入</h4>
<p>有时候,我们并不想对结构体字段类型和<code>mapinterface{}</code>的对应键值做强类型一致的校验。这时可以使用<code>WeakDecode/WeakDecodeMetadata</code>方法,它们会尝试做类型转换:</p>
<pre><code class="language-go">type Person struct {
Name string
Age int
Emails []string
}
func main() {
m := mapinterface{}{
"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())
}
}
</code></pre>
<p>虽然键<code>name</code>对应的值<code>123</code>是<code>int</code>类型,但是在<code>WeakDecode</code>中会将其转换为<code>string</code>类型以匹配<code>Person.Name</code>字段的类型。同样的,<code>age</code>的值<code>"18"</code>是<code>string</code>类型,在<code>WeakDecode</code>中会将其转换为<code>int</code>类型以匹配<code>Person.Age</code>字段的类型。 需要注意一点,如果类型转换失败了,<code>WeakDecode</code>同样会返回错误。例如将上例中的<code>age</code>设置为<code>"bad value"</code>,它就不能转为<code>int</code>类型,故而返回错误。</p>
<h4 id="212解码器">2.12、解码器</h4>
<p>除了上面介绍的方法外,<code>mapstructure</code>还提供了更灵活的解码器(<code>Decoder</code>)。可以通过配置<code>DecoderConfig</code>实现上面介绍的任何功能:</p>
<pre><code class="language-go">// 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
}
</code></pre>
<p>例子:</p>
<pre><code>type Person struct {
Name string
Ageint
}
func main() {
m := mapinterface{}{
"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())
}
}
</code></pre>
<p>这里用<code>Decoder</code>的方式实现了前面弱类型输入小节中的示例代码。实际上<code>WeakDecode</code>内部就是通过这种方式实现的,下面是<code>WeakDecode</code>的源码:</p>
<pre><code class="language-go">// 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)
}
</code></pre>
<p>再实际上,<code>Decode/DecodeMetadata/WeakDecodeMetadata</code>内部都是先设置<code>DecoderConfig</code>的对应字段,然后创建<code>Decoder</code>对象,最后调用其<code>Decode</code>方法实现的。</p>
<h4 id="参考链接">参考链接:</h4>
<p>Godoc</p>
<p>Go 每日一库之 mapstructure</p><br><br>
来源:https://www.cnblogs.com/huageyiyangdewo/p/17661013.html
頁:
[1]