您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > javascript

使用 Mapstructure 解析 Json,你学会了吗?

时间:2023-12-27 13:21:24  来源:  作者:爱发白日梦的后端

背景

前几天群里的小伙伴问了一个这样的问题:

使用 Mapstructure 解析 Json,你学会了吗?图片

其实质就是在面对 value 类型不确定的情况下,怎么解析这个 json?

我下意识就想到了 [mapstructure](https://Github.com/mitchellh/mapstructure) 这个库,它可以帮助我们类似 php 那样去处理弱类型的结构。

介绍

先来介绍一下 mapstructure 这个库主要用来做什么的吧,官网是这么介绍的:

mapstructure 是一个 Go 库,用于将通用映射值解码为结构,反之亦然,同时提供有用的错误处理。

该库在解码数据流(JSON、Gob 等)中的值时最为有用,因为在读取部分数据之前,您并不十分清楚底层数据的结构。因此,您可以读取 map[string]interface{} 并使用此库将其解码为适当的本地 Go 底层结构。

简单来说,它擅长解析一些我们并不十分清楚底层数据结构的数据流到我们定义的结构体中。

下面我们通过几个例子来简单介绍一下 mapstructure 怎么使用。

例子

普通形式

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("%#vn", result)
}

输出:

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{} 之后,再将其映射到我们的结构体中。

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("%#vn", result)
}

输出:

main.Person{Name:"Tim", Age:31, Gender:"male"}

嵌入式结构

mapstructure 允许我们压缩多个嵌入式结构,并通过 squash 标签进行处理。

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, %sn", result.FirstName, result.LastName, result.City)
}

输出:

Tim Liu, China, Guangdong

在这个例子中, Person 里面有着 Location 和 Family 的嵌入式结构体,通过 squash 标签进行压缩,从而达到平铺的作用。

元数据

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: %#vn", result, md.Keys, md.Unused, md.Unset)
}

输出:

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 标签来解决。

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("%+vn", result)
}

输出:

&map[Age:0 FirstName:Somebody]

这里我们可以看到 *Family 和 *Location 都被设置了 omitempty,所以在解析过程中会忽略掉空值。而 Age 没有设置,并且 input 中没有对应的 value,所以在解析中使用对应类型的零值来表达,而 int 类型的零值就是 0。

剩余字段

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("%#vn", result)
}

输出:

main.Person{Name:"Tim", Age:31, Other:map[string]interface {}{"email":"one@gmail.com", "gender":"male"}}

从代码可以看到 Other 字段被设置了 remain,这意味着 input 中没有正确映射的字段都会被放到 Other 中,从输出可以看到,email 和 gender 已经被正确的放到 Other 中了。

自定义标签

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("%#vn", result)
}

输出:

main.Person{Name:"Tim", Age:31}

在 Person 结构中,我们将 person_name 和 person_age 分别映射到 Name 和 Age 中,从而达到在不破坏结构的基础上,去正确的解析。

弱类型解析

正如前面所说,mapstructure 提供了类似 PHP 解析弱类型结构的方法。

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("%#vn", result)
}

输出:

main.Person{Name:"123", Age:31, Emails:[]string{}}

从代码可以看到,input 中的 name、age 和 Person 结构体中的 Name、Age 类型不一致,而 email 更是离谱,一个字符串数组,一个是 map。

但是我们通过自定义 DecoderConfig,将 WeaklyTypedInput 设置成 true 之后,mapstructure 很容易帮助我们解决这类弱类型的解析问题。

但是也不是所有问题都能解决,通过源码我们可以知道有如下限制:

//   - 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 错误提示非常的友好,下面我们来看看遇到错误时,它是怎么提示的。

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())
 }
}

输出:

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 的强大之处,很好的帮我们解决了实实在在的问题,也在节省我们的开发成本。

但是从源码来看,内部使用了大量的反射,这可能会对一些特殊场景带来性能隐患。所以大家在使用的时候,一定要充分考虑产品逻辑以及场景。

以下贴一小段删减过的源码:

// 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
}


Tags:Json   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
如何在Rust中操作JSON,你学会了吗?
sonic-rs ​还具有一些额外的方法来进行惰性评估和提高速度。例如,如果我们想要一个 JSON​ 字符串文字,我们可以在反序列化时使用 LazyValue​ 类型将其转换为一个仍然带有斜...【详细内容】
2024-02-27  Search: Json  点击:(47)  评论:(0)  加入收藏
.NET配置文件大揭秘:轻松读取JSON、XML、INI和环境变量
概述:.NET中的IConfiguration接口提供了一种多源读取配置信息的灵活机制,包括JSON、XML、INI文件和环境变量。通过示例,清晰演示了从这些不同源中读取配置的方法,使配置获取变得...【详细内容】
2023-12-28  Search: Json  点击:(92)  评论:(0)  加入收藏
使用 Mapstructure 解析 Json,你学会了吗?
背景前几天群里的小伙伴问了一个这样的问题:图片其实质就是在面对 value 类型不确定的情况下,怎么解析这个 json?我下意识就想到了 [mapstructure](https://github.com/mitchel...【详细内容】
2023-12-27  Search: Json  点击:(150)  评论:(0)  加入收藏
JSON非常慢:这里有更快的替代方案!
是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探...【详细内容】
2023-11-21  Search: Json  点击:(251)  评论:(0)  加入收藏
Json格式弊端及优化方案
Json介绍Json(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于前后端数据传输和存储。它使用简洁的文本格式来表示结构化的数据,易于阅读和编写,并且可以被多种编...【详细内容】
2023-11-13  Search: Json  点击:(295)  评论:(0)  加入收藏
web服务器json-serve详解
简介JSON-Server 是一个 Node 模块,运行 Express 服务器,你可以指定一个 json 文件作为 api 的数据源。一、安装 JSON-Serve (Install JSON Server)使用 npm 或 yarn 工具安装...【详细内容】
2023-11-09  Search: Json  点击:(318)  评论:(0)  加入收藏
Python文件操作:JSON、CSV、TSV、Excel和Pickle文件序列化
文件操作是Python编程的重要部分,它涉及处理各种文件格式,包括JSON、CSV、TSV、Excel和Pickle。一、JSON文件操作1.1 什是JSON?JSON(JavaScript Object Notation)是一种轻量级数...【详细内容】
2023-10-26  Search: Json  点击:(77)  评论:(0)  加入收藏
程序开发中是使用XML还是使用JSON作为数据传输格式好?
在程序开发中,使用XML还是JSON作为传输对象是一个常见的问题。两者都是常用的数据交换格式,但在不同的情况下,使用XML或JSON可能会有不同的优势和适用性。XML(可扩展标记语言)是...【详细内容】
2023-10-24  Search: Json  点击:(25)  评论:(0)  加入收藏
程序开发中使用XML还是JSON作为数据传输格式好?
在程序开发中,使用XML还是JSON作为传输对象是一个常见的问题。两者都是常用的数据交换格式,但在不同的情况下,使用XML或JSON可能会有不同的优势和适用性。XML(可扩展标记语言)是...【详细内容】
2023-10-24  Search: Json  点击:(187)  评论:(0)  加入收藏
Python开发者的宝典:CSV和JSON数据处理技巧大公开!
在Python中处理CSV和JSON数据时,需要深入了解这两种数据格式的读取、写入、处理和转换方法。下面将详细介绍如何在Python中处理CSV和JSON数据,并提供一些示例和最佳实践。CSV...【详细内容】
2023-10-17  Search: Json  点击:(217)  评论:(0)  加入收藏
▌简易百科推荐
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  前端新世界  微信公众号  Tags:JavaScript   点击:(4)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践——如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  前端新世界  微信公众号  Tags:JavaScript   点击:(25)  评论:(0)  加入收藏
又出新JS运行时了!JS运行时大盘点
Node.js是基于Google V8引擎的JavaScript运行时,以非阻塞I/O和事件驱动架构为特色,实现全栈开发。它跨平台且拥有丰富的生态系统,但也面临安全性、TypeScript支持和性能等挑战...【详细内容】
2024-03-21  前端充电宝  微信公众号  Tags:JS   点击:(22)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  前端历险记  微信公众号  Tags:JavaScript   点击:(20)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  WangLiwen    Tags:JavaScript   点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  京东云开发者    Tags:JavaScript   点击:(2)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06    51CTO  Tags:JavaScript   点击:(52)  评论:(0)  加入收藏
JS小知识,使用这6个小技巧,避免过多的使用 if 语句
最近在重构我的代码时,我注意到早期的代码使用了太多的 if 语句,达到了我以前从未见过的程度。这就是为什么我认为分享这些可以帮助我们避免使用过多 if 语句的简单技巧很重要...【详细内容】
2024-01-30  前端达人  今日头条  Tags:JS   点击:(56)  评论:(0)  加入收藏
18个JavaScript技巧:编写简洁高效的代码
本文翻译自 18 JavaScript Tips : You Should Know for Clean and Efficient Code,作者:Shefali, 略有删改。在这篇文章中,我将分享18个JavaScript技巧,以及一些你应该知道的示例...【详细内容】
2024-01-30  南城大前端  微信公众号  Tags:JavaScript   点击:(65)  评论:(0)  加入收藏
使用 JavaScript 清理我的 200GB iCloud,有了一个意外发现!
本文作者在综合成本因素之下,决定用 Java 脚本来清理一下自己的 iCloud,结果却有了一个意外发现,即在 iCloud 中上传同一个视频和删除此视频之后,iCloud 的空间并不一致,这到底是...【详细内容】
2024-01-11    CSDN  Tags:JavaScript   点击:(97)  评论:(0)  加入收藏
站内最新
站内热门
站内头条