返回介绍

8.3 编码/解码

发布于 2024-10-11 12:39:10 字数 10533 浏览 0 评论 0 收藏 0

许多程序都需要处理或者发布数据,不管这个程序是要使用数据库,进行网络调用,还是与分布式系统打交道。如果程序需要处理 XML 或者 JSON,可以使用标准库里名为 xmljson 的包,它们可以处理这些格式的数据。如果想实现自己的数据格式的编解码,可以将这些包的实现作为指导。

在今天,JSON 远比 XML 流行。这主要是因为与 XML 相比,使用 JSON 需要处理的标签更少。而这就意味着网络传输时每个消息的数据更少,从而提升整个系统的性能。而且,JSON 可以转换为 BSON(Binary JavaScript Object Notation,二进制 JavaScript 对象标记),进一步缩小每个消息的数据长度。因此,我们会学习如何在 Go 应用程序里处理并发布 JSON。处理 XML 的方法也很类似。

8.3.1 解码 JSON

我们要学习的处理 JSON 的第一个方面是,使用 json 包的 NewDecoder 函数以及 Decode 方法进行解码。如果要处理来自网络响应或者文件的 JSON,那么一定会用到这个函数及方法。让我们来看一个处理 Get 请求响应的 JSON 的例子,这个例子使用 http 包获取 Google 搜索 API 返回的 JSON。代码清单 8-23 展示了这个响应的内容。

代码清单 8-23 Google 搜索 API 的 JSON 响应例子

{
  "responseData": {
    "results": [
      {
        "GsearchResultClass": "GwebSearch",
        "unescapedUrl": "https://www.reddit.com/r/golang",
        "url": "https://www.reddit.com/r/golang",
        "visibleUrl": "www.reddit.com",
        "cacheUrl": "http://www.google.com/search?q=cache:W...",
        "title": "r/\u003cb\u003eGolang\u003c/b\u003e - Reddit",
        "titleNoFormatting": "r/Golang - Reddit",
        "content": "First Open Source \u003cb\u003eGolang\u..."
      },
      {
        "GsearchResultClass": "GwebSearch",
        "unescapedUrl": "http://tour.golang.org/",
        "url": "http://tour.golang.org/",
        "visibleUrl": "tour.golang.org",
        "cacheUrl": "http://www.google.com/search?q=cache:O...",
        "title": "A Tour of Go",
        "titleNoFormatting": "A Tour of Go",
        "content": "Welcome to a tour of the Go programming ..."
      }
    ]
  }
}

代码清单 8-24 给出的是如何获取响应并将其解码到一个结构类型里的例子。

代码清单 8-24 listing24.go

01 // 这个示例程序展示如何使用 json 包和 NewDecoder 函数
02 // 来解码 JSON 响应
03 package main
04
05 import (
06   "encoding/json"
07   "fmt"
08   "log"
09   "net/http"
10 )
11
12 type (
13   // gResult 映射到从搜索拿到的结果文档
14   gResult struct {
15     GsearchResultClass string `json:"GsearchResultClass"`
16     UnescapedURL    string `json:"unescapedUrl"`
17     URL         string `json:"url"`
18     VisibleURL     string `json:"visibleUrl"`
19     CacheURL       string `json:"cacheUrl"`
20     Title        string `json:"title"`
21     TitleNoFormatting string `json:"titleNoFormatting"`
22     Content       string `json:"content"`
23   }
24
25   // gResponse 包含顶级的文档
26   gResponse struct {
27     ResponseData struct {
28       Results []gResult `json:"results"`
29     } `json:"responseData"`
30   }
31 )
32
33 func main() {
34   uri := "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&rsz=8&q=golang"
35
36   // 向 Google 发起搜索
37   resp, err := http.Get(uri)
38   if err != nil {
39     log.Println("ERROR:", err)
40     return
41   }
42   defer resp.Body.Close()
43
44   // 将 JSON 响应解码到结构类型
45   var gr gResponse
46   err = json.NewDecoder(resp.Body).Decode(&gr)
47   if err != nil {
48     log.Println("ERROR:", err)
49     return
50   }
51
52   fmt.Println(gr)
53 }

代码清单 8-24 中代码的第 37 行,展示了程序做了一个 HTTP Get 调用,希望从 Google 得到一个 JSON 文档。之后,在第 46 行使用 NewDecoder 函数和 Decode 方法,将响应返回的 JSON 文档解码到第 26 行声明的一个结构类型的变量里。在第 52 行,将这个变量的值写到 stdout

如果仔细看第 26 行和第 14 行的 gResponsegResult 的类型声明,你会注意到每个字段最后使用单引号声明了一个字符串。这些字符串被称作 标签 (tag),是提供每个字段的元信息的一种机制,将 JSON 文档和结构类型里的字段一一映射起来。如果不存在 标签 ,编码和解码过程会试图以大小写无关的方式,直接使用字段的名字进行匹配。如果无法匹配,对应的结构类型里的字段就包含其零值。

执行 HTTP Get 调用和解码 JSON 到结构类型的具体技术细节都由标准库包办了。让我们看一下标准库里 NewDecoder 函数和 Decode 方法的声明,如代码清单 8-25 所示。

代码清单 8-25 golang.org/src/encoding/json/stream.go

// NewDecoder 返回从 r 读取的解码器
//
// 解码器自己会进行缓冲,而且可能会从 r 读比解码 JSON 值
// 所需的更多的数据
func NewDecoder(r io.Reader) *Decoder

// Decode 从自己的输入里读取下一个编码好的 JSON 值, 
// 并存入 v 所指向的值里
//
// 要知道从 JSON 转换为 Go 的值的细节, 
// 请查看 Unmarshal 的文档
func (dec *Decoder) Decode(v interface{}) error

在代码清单 8-25 中可以看到 NewDecoder 函数接受一个实现了 io.Reader 接口类型的值作为参数。在下一节,我们会更详细地介绍 io.Readerio.Writer 接口,现在只需要知道标准库里的许多不同类型,包括 http 包里的一些类型,都实现了这些接口就行。只要类型实现了这些接口,就可以自动获得许多功能的支持。

函数 NewDecoder 返回一个指向 Decoder 类型的指针值。由于 Go 语言支持复合语句调用,可以直接调用从 NewDecoder 函数返回的值的 Decode 方法,而不用把这个返回值存入变量。在代码清单 8-25 里,可以看到 Decode 方法接受一个 interface{} 类型的值做参数,并返回一个 error 值。

在第 5 章中曾讨论过,任何类型都实现了一个空接口 interface{} 。这意味着 Decode 方法可以接受任意类型的值。使用反射, Decode 方法会拿到传入值的类型信息。然后,在读取 JSON 响应的过程中, Decode 方法会将对应的响应解码为这个类型的值。这意味着用户不需要创建对应的值, Decode 会为用户做这件事情,如代码清单 8-26 所示。

在代码清单 8-26 中,我们向 Decode 方法传入了指向 gResponse 类型的指针变量的地址,而这个地址的实际值为 nil 。该方法调用后,这个指针变量会被赋给一个 gResponse 类型的值,并根据解码后的 JSON 文档做初始化。

代码清单 8-26 使用 Decode 方法

var gr *gResponse
err = json.NewDecoder(resp.Body).Decode(&gr)

有时,需要处理的 JSON 文档会以 string 的形式存在。在这种情况下,需要将 string 转换为 byte 切片( []byte ),并使用 json 包的 Unmarshal 函数进行反序列化的处理,如代码清单 8-27 所示。

代码清单 8-27 listing27.go

01 // 这个示例程序展示如何解码 JSON 字符串
02 package main
03
04 import (
05   "encoding/json"
06   "fmt"
07   "log"
08 )
09
10 // Contact 结构代表我们的 JSON 字符串
11 type Contact struct {
12   Name  string `json:"name"`
13   Title  string `json:"title"`
14   Contact struct {
15     Home string `json:"home"`
16     Cell string `json:"cell"`
17   } `json:"contact"`
18 }
19
20 // JSON 包含用于反序列化的演示字符串
21 var JSON = `{
22   "name": "Gopher",
23   "title": "programmer",
24   "contact": {
25     "home": "415.333.3333",
26     "cell": "415.555.5555"
27   }
28 }`
29
30 func main() {
31   // 将 JSON 字符串反序列化到变量
32   var c Contact
33   err := json.Unmarshal([]byte(JSON), &c)
34   if err != nil {
35     log.Println("ERROR:", err)
36     return
37   }
38
39   fmt.Println(c)
40 }

在代码清单 8-27 中,我们的例子将 JSON 文档保存在一个字符串变量里,并使用 Unmarshal 函数将 JSON 文档解码到一个结构类型的值里。如果运行这个程序,会得到代码清单 8-28 所示的输出。

代码清单 8-28 listing27.go 的输出

{Gopher programmer {415.333.3333 415.555.5555}}

有时,无法为 JSON 的格式声明一个结构类型,而是需要更加灵活的方式来处理 JSON 文档。在这种情况下,可以将 JSON 文档解码到一个 map 变量中,如代码清单 8-29 所示。

代码清单 8-29 listing29.go

01 // 这个示例程序展示如何解码 JSON 字符串
02 package main
03
04 import (
05   "encoding/json"
06   "fmt"
07   "log"
08 )
09
10 // JSON 包含要反序列化的样例字符串
11 var JSON = `{
12   "name": "Gopher",
13   "title": "programmer",
14   "contact": {
15     "home": "415.333.3333",
16     "cell": "415.555.5555"
17   }
18 }`
19
20 func main() {
21   // 将 JSON 字符串反序列化到 map 变量
22   var c map[string]interface{}
23   err := json.Unmarshal([]byte(JSON), &c)
24   if err != nil {
25     log.Println("ERROR:", err)
26     return
27   }
28
29   fmt.Println("Name:", c["name"])
30   fmt.Println("Title:", c["title"])
31   fmt.Println("Contact")
32   fmt.Println("H:", c["contact"].(map[string]interface{})["home"])
33   fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])
34 }

代码清单 8-29 中的程序修改自代码清单 8-27,将其中的结构类型变量替换为 map 类型的变量。变量 c 声明为一个 map 类型,其键是 string 类型,其值是 interface{} 类型。这意味着这个 map 类型可以使用任意类型的值作为给定键的值。虽然这种方法为处理 JSON 文档带来了很大的灵活性,但是却有一个小缺点。让我们看一下访问 contact 子文档的 home 字段的代码,如代码清单 8-30 所示。

代码清单 8-30 访问解组后的映射的字段的代码

fmt.Println("\tHome:", c["contact"].(map[string]interface{})["home"])

因为每个键的值的类型都是 interface{} ,所以必须将值转换为合适的类型,才能处理这个值。代码清单 8-30 展示了如何将 contact 键的值转换为另一个键是 string 类型,值是 interface{} 类型的 map 类型。这有时会使映射里包含另一个文档的 JSON 文档处理起来不那么友好。但是,如果不需要深入正在处理的 JSON 文档,或者只打算做很少的处理,因为不需要声明新的类型,使用 map 类型会很快。

8.3.2 编码 JSON

我们要学习的处理 JSON 的第二个方面是,使用 json 包的 MarshalIndent 函数进行编码。这个函数可以很方便地将 Go 语言的 map 类型的值或者结构类型的值转换为易读格式的 JSON 文档。 序列化 (marshal)是指将数据转换为 JSON 字符串的过程。下面是一个将 map 类型转换为 JSON 字符串的例子,如代码清单 8-31 所示。

代码清单 8-31 listing31.go

01 // 这个示例程序展示如何序列化 JSON 字符串
02 package main
03
04 import (
05   "encoding/json"
06   "fmt"
07   "log"
08 )
09
10 func main() {
11   // 创建一个保存键值对的映射
12   c := make(map[string]interface{})
13   c["name"] = "Gopher"
14   c["title"] = "programmer"
15   c["contact"] = map[string]interface{}{
16     "home": "415.333.3333",
17     "cell": "415.555.5555",
18   }
19
20   // 将这个映射序列化到 JSON 字符串
21   data, err := json.MarshalIndent(c, "", "  ")
22   if err != nil {
23     log.Println("ERROR:", err)
24     return
25   }
26
27   fmt.Println(string(data))
28 }

代码清单 8-31 展示了如何使用 json 包的 MarshalIndent 函数将一个 map 值转换为 JSON 字符串。函数 MarshalIndent 返回一个 byte 切片,用来保存 JSON 字符串和一个 error 值。下面来看一下 json 包中 MarshalIndent 函数的声明,如代码清单 8-32 所示。

代码清单 8-32 golang.org/src/encoding/json/encode.go

// MarshalIndent 很像 Marshal,只是用缩进对输出进行格式化
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {

MarshalIndent 函数里再一次看到使用了空接口类型 interface{} 。函数 MarshalIndent 会使用反射来确定如何将 map 类型转换为 JSON 字符串。

如果不需要输出带有缩进格式的 JSON 字符串, json 包还提供了名为 Marshal 的函数来进行解码。这个函数产生的 JSON 字符串很适合作为在网络响应(如 Web API)的数据。函数 Marshal 的工作原理和函数 MarshalIndent 一样,只不过没有用于前缀 prefix 和缩进 indent 的参数。

8.3.3 结论

在标准库里都已经提供了处理 JSON 和 XML 格式所需要的诸如解码、反序列化以及序列化数据的功能。随着每次 Go 语言新版本的发布,这些包的执行速度也越来越快。这些包是处理 JSON 和 XML 的最佳选择。由于有反射包和标签的支持,可以很方便地声明一个结构类型,并将其中的字段映射到需要处理和发布的文档的字段。由于 json 包和 xml 包都支持 io.Readerio.Writer 接口,用户不用担心自己的 JSON 和 XML 文档源于哪里。所有的这些特性都让处理 JSON 和 XML 变得很容易。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文