在GO中表示JSON结构

2020-05-17 07:27:50

经过一段时间的阅读和回答关于围棋的堆栈溢出问题后,去年我写了一篇Go JSON Cookbook帖子,总结了围棋和JSON的一些常见问题。从那时起,我注意到一种与JSON相关的特殊问题不断出现,所以我想在一个单独的帖子中直接解决这个问题,这个帖子将比大量食谱中的特定部分更容易链接到。

这个问题通常伴随着一段JSON数据,询问如何将其映射到GO上。通常,JSON数据是某种API响应,但实际上可以是任何东西。

这里是我们将在这篇文章中使用的JSON示例。它比你在野外遇到的要简单得多,但它应该能很好地推广到其他结构:

{";属性";:[{";名称";:";颜色";,";计数";:9},{";名称";:";家族";,";计数";:127}],";水果";::[{";名称";:";橙色";,";甜度";:12.3,";属性";:{";家庭";:";柑橘";}}]}。

第一种方法(在大多数情况下应该是默认的)是完全保真结构表示。这意味着从JSON对象到GO结构的映射是完全类型安全的。手工编写这样的结构类型已经很容易了,但是有一些工具可以让这件事变得更容易。例如,下面是由JSON-to-Go生成的结构:

类型AutoGenerated struct{Attrs[]struct{name string`json:";name";`count int`json:";count";attrs";`fruits[]struct{name string`json:";name";`Sweetness float64`json:";Sweetness";`Attr struct{Family String`json:&&。`}`json:";水果";`}。

将我们的JSON解析到此结构中并迭代所有水果以找到它们的甜度是一件简单的事情[1]:

如果err:=json,则自动生成var ag。Unmarshal(jsonText,&;ag);err!=nil{log.。致命(错误)}FOR_,FOR:=RANGE AG.。水果{fmt.。Printf(";%s->;%f\n&34;,水果。名字,水果。甜度)}

这并不比您在动态语言中发现的更冗长,但是它完全是类型安全的。此外,解析时会自动验证JSON以匹配外部结构。

Go JSON包支持使用反射解析成通用表示。这类似于动态语言中的AMAP-of-List-of-map或类似的命名法;通常,假设顶级JSON字段是字符串(很安全),我们可以要求json.Unmarshal将JSON解析为map[string]接口{}。在幕后,解析器将根据它在JSON中遇到的内容创建具体的GO类型,但是GO编译器无法在编译时知道这些类型是什么,因此我们将使用json.Unmarshal将JSON解析成一个map[string]接口{}。在幕后,解析器将根据它在JSON中遇到的内容创建具体的GO类型,但是GO编译器在编译时无法知道这些类型是什么,因此我们将。

下面是一个完整的示例,它打印出所有的水果名称和它们的甜度,而不使用struct声明:

如果err:=json,则变量m映射[字符串]接口{}。Unmarshal(jsonText,&;m);err!=nil{log.。致命(错误)}水果,OK:=m[";水果";]如果!好的{记录。FATAL(";';水果';字段未找到";)}fSlice,OK:=水果。([]interface{})if!好的{记录。FATAL(";';FIELS';FIELD不是切片";)}for_,f:=range fSlice{fmap,ok:=f.(map[String]interface{})if!好的{记录。致命(";';水果';元素不是地图";)}名称,OK:=FMAP[";名称";]如果!好的{记录。FATAL(";FIELS元素没有';Name';field";)}Sweetness,OK:=FMAP[";Sweetness";]if!好的{记录。致命(#34;水果元素没有甜度#34;字段#34;)}FMT。Printf(";%s->;%f\n&34;,名称,甜度)}。

注意这里需要进行大量的错误检查--我们被迫仔细检查每个字段的存在和我们需要的每个值的类型,才能找到我们感兴趣的实际字段/值对。

这看起来似乎有很多代码,但这与编写类似代码时动态语言在幕后所做的非常相似。例如,我们可以编写相同的代码,如下所示:

如果err:=json,则变量m映射[字符串]接口{}。Unmarshal(jsonText,&;m);err!=nil{log.。致命(Err)}水果:=m[";水果";].([]interface{})for_,f:=Range Fruits{水果:=f.(map[string]interface{})fmt。Printf(";%s->;%f\n&34;,水果[";名称&34;],水果[";甜度";])}。

这与它在Python中的外观没有太大不同(除了类型断言)。注意到少了什么吗?错误检查。相反,如果情况不符合预期,Go将在运行时出现恐慌。这再次类似于通常使用异常的动态语言。在Go中,您可以通过让代码在出现错误时死机并使用recover在更高级别捕获死机来达到大致相同的效果;这很吸引人,但也有其自身的问题。

所以代码的冗长并不是因为它的非类型化性质;它主要是由于GO显式的错误检查规范。

到目前为止,我们已经看到了JSON对象的完全类型安全表示和非类型化表示。一个有趣的折衷办法是使用混合表示,在这种表示中,我们以一种非类型化的方式进行,直到我们找到JSON中有趣的部分,然后我们想要用一个实际的结构来表示它。

变量m映射[字符串]json。如果err:=json,则为RawMessage。Unmarshal(jsonText,&;m);err!=nil{log.。致命(错误)}水果原始,OK:=m[";水果";]如果!好的{记录。致命(";预计会找到';水果';";)}可变水果[]水果,如果错误:=json。Unmarshal(水果原始,&;水果);err!=nil{log.。致命(错误)}for_,水果:=范围水果{fmt.。Printf(";%s->;%f\n&34;,水果。名字,水果。甜度)}。

注意json.RawMessage的用法。我们在第一个调用中告诉json.Unmarshal的是:使用字符串键将对象解析为AMAP,但不解析这些值。这样做很重要,因为如果我们将值设置为interface{},json.Unmarshal会将它们解析成具体类型,或映射/切片,就像我们在通用表示中看到的那样。取而代之的是,将值保持为json.RawMessage允许我们延迟解析,直到我们知道要解析到的更具体的类型-比如本例中的Fruit。

这种延迟的解析可以带来额外的好处,比如性能。我们可能正在解析一个很大的JSON对象,但只对单个键感兴趣;使用map[string]json.RawMessage告诉解析器不要解析值。我们稍后只能解析感兴趣的值,而不会浪费其他值上的资源。

当我们对复杂的JSON结构中的一小部分感兴趣时,混合方法通常很有用,而我们并不想完全验证它。此外,一些JSON结构实际上没有匹配的静态类型(例如,字段可以基于其他字段具有不同的类型)。在这些情况下,使用JSON的全保真结构表示可能很麻烦,也可能不可行,而混合方法是一个很好的折衷方案,可以更方便地公开我们实际关心的数据片段。