> 关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!
## 背景
前几天群里的小伙伴问了一个这样的问题:
![Snipaste_2023-12-26_22-07-48.png](https://static.golangjob.cn/231227/386e2a7e14a3f08bf86c506939705fb7.png)
其实质就是在面对 `value` 类型不确定的情况下,怎么解析这个 json?
我下意识就想到了 `[mapstructure](https://github.com/mitchellh/mapstructure)` 这个库,它可以帮助我们类似 PHP 那样去处理弱类型的结构。
## 介绍
先来介绍一下 `mapstructure` 这个库主要用来做什么的吧,官网是这么介绍的:
> `mapstructure` 是一个 Go 库,用于将通用映射值解码为结构,反之亦然,同时提供有用的错误处理。
>
>
> 该库在解码数据流(JSON、Gob 等)中的值时最为有用,因为在读取部分数据之前,您并不十分清楚底层数据的结构。因此,您可以读取 `map[string]interface{}` 并使用此库将其解码为适当的本地 Go 底层结构。
>
简单来说,它擅长解析一些我们并不十分清楚底层数据结构的数据流到我们定义的结构体中。
下面我们通过几个例子来简单介绍一下 `mapstructure` 怎么使用。
## 例子
### 普通形式
```go
func normalDecode() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
// 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
input := map[string]interface{}{
"name": "Tim",
"age": 31,
"emails": []string{"one@gmail.com", "two@gmail.com", "three@gmail.com"},
"extra": map[string]string{
"twitter": "Tim",
},
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
```
输出:
```go
main.Person{Name:"Tim", Age:31, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Tim"}}
```
这个方式应该是我们最经常使用的,非常简单的将 `map[string]interface{}` 映射到我们的结构体中。
在这里,我们并没有指定每个 `field` 的 `tag`,让 `mapstructure` 自动去映射。
如果我们的 `input` 是一个 json 字符串,那么我们需要将 json 字符串解析为 `map[string]interface{}` 之后,再将其映射到我们的结构体中。
```go
func jsonDecode() {
var jsonStr = `{
"name": "Tim",
"age": 31,
"gender": "male"
}`
type Person struct {
Name string
Age int
Gender string
}
m := make(map[string]interface{})
err := json.Unmarshal([]byte(jsonStr), &m)
if err != nil {
panic(err)
}
var result Person
err = mapstructure.Decode(m, &result)
if err != nil {
panic(err.Error())
}
fmt.Printf("%#v\n", result)
}
```
输出:
```go
main.Person{Name:"Tim", Age:31, Gender:"male"}
```
### 嵌入式结构
`mapstructure` 允许我们压缩多个嵌入式结构,并通过 *`squash`* 标签进行处理。
```go
func embeddedStructDecode() {
// 使用 squash 标签允许压缩多个嵌入式结构。通过创建多种类型的复合结构并对其进行解码来演示此功能。
type Family struct {
LastName string
}
type Location struct {
City string
}
type Person struct {
Family `mapstructure:",squash"`
Location `mapstructure:",squash"`
FirstName string
}
input := map[string]interface{}{
"FirstName": "Tim",
"LastName": "Liu",
"City": "China, Guangdong",
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%s %s, %s\n", result.FirstName, result.LastName, result.City)
}
```
输出:
```go
Tim Liu, China, Guangdong
```
在这个例子中, `Person` 里面有着 `Location` 和 `Family` 的嵌入式结构体,通过 `squash` 标签进行压缩,从而达到平铺的作用。
### 元数据
```go
func metadataDecode() {
type Person struct {
Name string
Age int
Gender string
}
// 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
input := map[string]interface{}{
"name": "Tim",
"age": 31,
"email": "one@gmail.com",
}
// 对于元数据,我们制作了一个更高级的 DecoderConfig,以便我们可以更细致地配置所使用的解码器。在这种情况下,我们只是告诉解码器我们想要跟踪元数据。
var md mapstructure.Metadata
var result Person
config := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &result,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
panic(err)
}
if err = decoder.Decode(input); err != nil {
panic(err)
}
fmt.Printf("value: %#v, keys: %#v, Unused keys: %#v, Unset keys: %#v\n", result, md.Keys, md.Unused, md.Unset)
}
```
输出:
```go
value: main.Person{Name:"Tim", Age:31, Gender:""}, keys: []string{"Name", "Age"}, Unused keys: []string{"email"}, Unset keys: []string{"Gender"}
```
从这个例子我们可以看出,使用 `Metadata` 可以记录我们结构体以及 `map[string]interface{}` 的差异,相同的部分会正确映射到对应的字段中,而差异则使用了 `Unused` 和 `Unset` 来表达。
- Unused:map 中有着结构体所没有的字段。
- Unset:结构体中有着 map 中所没有的字段。
### 避免空值的映射
这里的使用其实和内置的 json 库使用方式是一样的,都是借助 `omitempty` 标签来解决。
```go
func omitemptyDecode() {
// 添加 omitempty 注释以避免空值的映射键
type Family struct {
LastName string
}
type Location struct {
City string
}
type Person struct {
*Family `mapstructure:",omitempty"`
*Location `mapstructure:",omitempty"`
Age int
FirstName string
}
result := &map[string]interface{}{}
input := Person{FirstName: "Somebody"}
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", result)
}
```
输出:
```go
&map[Age:0 FirstName:Somebody]
```
这里我们可以看到 `*Family` 和 `*Location` 都被设置了 `omitempty`,所以在解析过程中会忽略掉空值。而 `Age` 没有设置,并且 `input` 中没有对应的 `value`,所以在解析中使用对应类型的零值来表达,而 `int` 类型的零值就是 `0`。
### 剩余字段
```go
func remainDataDecode() {
type Person struct {
Name string
Age int
Other map[string]interface{} `mapstructure:",remain"`
}
input := map[string]interface{}{
"name": "Tim",
"age": 31,
"email": "one@gmail.com",
"gender": "male",
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
```
输出:
```go
main.Person{Name:"Tim", Age:31, Other:map[string]interface {}{"email":"one@gmail.com", "gender":"male"}}
```
从代码可以看到 `Other` 字段被设置了 `remain`,这意味着 `input` 中没有正确映射的字段都会被放到 `Other` 中,从输出可以看到,`email` 和 `gender` 已经被正确的放到 `Other` 中了。
### 自定义标签
```go
func tagDecode() {
// 请注意,结构类型中定义的 mapstructure 标签可以指示将值映射到哪些字段。
type Person struct {
Name string `mapstructure:"person_name"`
Age int `mapstructure:"person_age"`
}
input := map[string]interface{}{
"person_name": "Tim",
"person_age": 31,
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
```
输出:
```go
main.Person{Name:"Tim", Age:31}
```
在 `Person` 结构中,我们将 `person_name` 和 `person_age` 分别映射到 `Name` 和 `Age` 中,从而达到在不破坏结构的基础上,去正确的解析。
### 弱类型解析
正如前面所说,`mapstructure` 提供了类似 PHP 解析弱类型结构的方法。
```go
func weaklyTypedInputDecode() {
type Person struct {
Name string
Age int
Emails []string
}
// 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,由 PHP 等弱类型语言生成。
input := map[string]interface{}{
"name": 123, // number => string
"age": "31", // string => number
"emails": map[string]interface{}{}, // empty map => empty array
}
var result Person
config := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: &result,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
panic(err)
}
err = decoder.Decode(input)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", result)
}
```
输出:
```go
main.Person{Name:"123", Age:31, Emails:[]string{}}
```
从代码可以看到,`input` 中的 `name`、`age` 和 `Person` 结构体中的 `Name`、`Age` 类型不一致,而 `email` 更是离谱,一个字符串数组,一个是 `map`。
但是我们通过自定义 `DecoderConfig`,将 `WeaklyTypedInput` 设置成 `true` 之后,`mapstructure` 很容易帮助我们解决这类弱类型的解析问题。
但是也不是所有问题都能解决,通过源码我们可以知道有如下限制:
```go
// - bools to string (true = "1", false = "0")
// - numbers to string (base 10)
// - bools to int/uint (true = 1, false = 0)
// - strings to int/uint (base implied by prefix)
// - int to bool (true if value != 0)
// - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F,
// FALSE, false, False. Anything else is an error)
// - empty array = empty map and vice versa
// - negative numbers to overflowed uint values (base 10)
// - slice of maps to a merged map
// - single values are converted to slices if required. Each
// element is weakly decoded. For example: "4" can become []int{4}
// if the target type is an int slice.
```
大家使用这种弱类型解析的时候也需要注意。
### 错误处理
`mapstructure` 错误提示非常的友好,下面我们来看看遇到错误时,它是怎么提示的。
```go
func decodeErrorHandle() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
}
input := map[string]interface{}{
"name": 123,
"age": "bad value",
"emails": []int{1, 2, 3},
}
var result Person
err := mapstructure.Decode(input, &result)
if err != nil {
fmt.Println(err.Error())
}
}
```
输出:
```go
5 error(s) decoding:
* 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value'
* 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1'
* 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2'
* 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3'
* 'Name' expected type 'string', got unconvertible type 'int', value: '123'
```
这里的错误提示会告诉我们每个字段,字段里的值应该需要怎么表达,我们可以通过这些错误提示,比较快的去修复问题。
## 总结
从上面这些例子看看到 mapstructure 的强大之处,很好的帮我们解决了实实在在的问题,也在节省我们的开发成本。
但是从源码来看,内部使用了大量的反射,这可能会对一些特殊场景带来性能隐患。所以大家在使用的时候,一定要充分考虑产品逻辑以及场景。
以下贴一小段删减过的源码:
```go
// Decode decodes the given raw interface to the target pointer specified
// by the configuration.
func (d *Decoder) Decode(input interface{}) error {
return d.decode("", input, reflect.ValueOf(d.config.Result).Elem())
}
// Decodes an unknown data type into a specific reflection value.
func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error {
....
var err error
outputKind := getKind(outVal)
addMetaKey := true
switch outputKind {
case reflect.Bool:
err = d.decodeBool(name, input, outVal)
case reflect.Interface:
err = d.decodeBasic(name, input, outVal)
case reflect.String:
err = d.decodeString(name, input, outVal)
case reflect.Int:
err = d.decodeInt(name, input, outVal)
case reflect.Uint:
err = d.decodeUint(name, input, outVal)
case reflect.Float32:
err = d.decodeFloat(name, input, outVal)
case reflect.Struct:
err = d.decodeStruct(name, input, outVal)
case reflect.Map:
err = d.decodeMap(name, input, outVal)
case reflect.Ptr:
addMetaKey, err = d.decodePtr(name, input, outVal)
case reflect.Slice:
err = d.decodeSlice(name, input, outVal)
case reflect.Array:
err = d.decodeArray(name, input, outVal)
case reflect.Func:
err = d.decodeFunc(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return fmt.Errorf("%s: unsupported type: %s", name, outputKind)
}
// If we reached here, then we successfully decoded SOMETHING, so
// mark the key as used if we're tracking metainput.
if addMetaKey && d.config.Metadata != nil && name != "" {
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
}
return err
}
```
有疑问加站长微信联系(非本文作者)