JSON 处理篇(上):JSON 编解码基本使用入门

process-json-in-golang

Go 语言内置了 encoding/json 标准库对 JSON 进行支持,开发者可以通过它轻松生成和解析 JSON 格式数据,下面我们来简单演示下这个库的使用。

JSON 编码示例

我们可以通过 encoding/json 包提供的 Marshal 函数将数据编码为 JSON 文本。该函数的声明如下:

func Marshal(v interface{}) ([]byte, error)

传入参数 v 是空接口,意味着可以传入任何类型数据,如果编码成功返回对应的 JSON 格式文本,否则,通过第二个返回参数标识错误信息。

注:此函数功能可类比为 PHP 里面的 json_encode 函数。

假如有如下这样一个 User 类型的结构体:

type User struct { 
    Name string
    Website string
    Age  uint
    Male bool
    Skills []string
}

通过如下形式对其进行初始化:

user := User{
    "学院君",
		"https://xueyuanjun.com",
		18,
		true,
		[]string{"Golang", "PHP", "C", "Java", "Python"},
}

然后,我们就可以使用 json.Marshal() 函数将上述 user 实例编码为 JSON 文本:

u, err := json.Marshal(user)

我们编写完整的示例代码如下:

# src/note/json/basic.go
package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name string
	Website string
	Age  uint
	Male bool
	Skills []string
}

func main()  {
	user := User{
		"学院君",
		"https://xueyuanjun.com",
		18,
		true,
		[]string{"Golang", "PHP", "C", "Java", "Python"},
	}

	u, err := json.Marshal(user)
	if err != nil {
		fmt.Printf("JSON 编码失败:%v\n", err)
		return
	}

	fmt.Printf("JSON 格式数据:%s\n", u)
}

如果编码成功,则 err 值为 nil,打印编码后数据 u 结果如下:

JSON 格式数据:{"Name":"学院君","Website":"https://xueyuanjun.com","Age":18,"Male":true,"Skills":["Golang","PHP","C","Java","Python"]}

底层实现逻辑是当我们调用 json.Marshal(user) 语句时,会递归遍历 user 对象,如果发现 user 这个数据结构实现了 json.Marshaler 接口且包含有效的值,Marshal() 就会调用其 MarshalJSON() 方法将该数据结构生成 JSON 格式文本。

数据类型映射

除了 channelcomplex 和函数这几种类型外,Go 语言的大多数数据类型都可以转化为有效的 JSON 文本。如果转化前的数据结构中出现指针,那么将会转化指针所指向的值,如果指针指向的是零值,那么 null 将作为转化后的结果输出。

在 Go 语言中,JSON 转化前后的数据类型映射如下:

  • 布尔值转化为 JSON 后还是布尔类型。
  • 浮点数和整型会被转化为 JSON 里边的常规数字。
  • 字符串将以 UTF-8 编码转化输出为 Unicode 字符集的字符串,特殊字符比如将会被转义为 \u003c
  • 数组和切片会转化为 JSON 里边的数组,但 []byte 类型的值将会被转化为 Base64 编码后的字符串,slice 类型的零值会被转化为 null
  • 结构体会转化为 JSON 对象,并且只有结构体里边以大写字母开头的可被导出的字段才会被转化输出,而这些可导出的字段会作为 JSON 对象的字符串索引。
  • 转化一个 map 类型的数据结构时,该数据的类型必须是 map[string]T(T 可以是 encoding/json 包支持的任意数据类型)。

JSON 解码示例

json.Marshal() 相对,我们可以使用 json.Unmarshal() 函数将 JSON 文本解码为 Go 语言对应的数据结构。json.Unmarshal() 函数的原型如下:

func Unmarshal(data []byte, v interface{}) error

该函数的第一个参数是待解码的 JSON 格式文本,第二个参数表示解码结果映射的目标类型数据结构(比如上面的 User 结构体)。

注:此函数可类比为 PHP 中的 json_decode 函数。

要解码 JSON 数据,首先需要在 Go 代码中声明这样一个目标类型的实例对象,用于存放解码后的值:

var user User

然后调用 json.Unmarshal() 函数,将 []byte 类型的 JSON 数据作为第一个参数传入,将 user 实例变量的指针作为第二个参数传入:

err := json.Unmarshal(u, &user)

如果 u 是一个有效的 JSON 数据并能和 user 结构对应起来,那么 JSON 解码后的值将会一一存放到 user 结构体对应字段中。

我们编写 JSON 解码示例代码如下:

# src/note/json/basic.go
...

func main()  {
	...

	u, err := json.Marshal(user)
	...

	var user2 User
	err = json.Unmarshal(u, &user2)
	if err != nil {
		fmt.Printf("JSON 解码失败:%v\n", err)
		return
	}

	fmt.Printf("JSON 解码结果: %#v\n", user2)
}

解码成功后的 user2 数据如下:

JSON 解码结果: main.User{Name:"学院君", Website:"https://xueyuanjun.com", Age:0x12, Male:true, Skills:[]string{"Golang", "PHP", "C", "Java", "Python"}}

数据类型映射

可以看到 json.Unmarshal() 比 PHP 的 json_decode 函数功能更强大,可以还原出原始的数据类型,那么,Go 语言是如何将 JSON 数据解码后的值一一映射到一个数据结构中的相应字段呢?

实际上,json.Unmarshal() 函数会根据一个约定的顺序查找目标结构中的字段,如果找到一个即发生匹配。假设某个 JSON 对象有一个名为 Foo 的索引(不区分大小写),要将 Foo 所对应的值填充到目标结构体的目标字段上,json.Unmarshal() 将会遵循如下顺序进行查找匹配:

  1. 一个包含 Foo 标签的字段(不区分大小写);
  2. 一个名为 Foo 或者除了首字母其他字母不区分大小写的名为 Foo 的字段(这些字段在类型声明中必须都是以大写字母开头、可被外部访问的公开字段)。

后面两个比较好理解,第一个我们在微服务架构教程中通过 protoc 生成的原型文件里面经常可以看到:

type User struct {
    Id  string `protobuf:"bytes,1,opt,name=id,proto3" json:"id"`
    Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name"`
    Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email"`
    Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password"`
}

比如这里的 Name 被打上 json:"name" 标签。

当 JSON 数据的结构和 Go 语言里边的目标类型的结构对不上时,会发生什么呢?示例代码如下:

u2 := []byte(`{"name": "学院君", "website": "https://xueyuanjun.com", "alias": "Laravel学院"}`)
var user3 User
err = json.Unmarshal(u2, &user3)
if err != nil {
fmt.Printf("JSON 解码失败:%v\n", err)
	return
}
fmt.Printf("JSON 解码结果: %#v\n", user3)

上述代码的打印结果如下:

JSON 解码结果: main.User{Name:"学院君", Website:"https://xueyuanjun.com", Age:0x0, Male:false, Skills:[]string(nil)}

可以看到,如果 JSON 中的字段在 Go 语言对应目标类型中不存在,json.Unmarshal() 函数在解码过程中会丢弃该字段,在上面这段示例代码中,由于 Alias 字段并没有在 User 类型中定义,所以会被忽略,只有 NameWebsite 这两个字段的值才会被填充到 user3 中。这个特性让我们可以从同一段 JSON 数据中筛选指定的值填充到多个不同的 Go 语言类型中。

对于 JSON 中没有而 User 中定义的字段,会以对应数据类型的默认值填充,比如上述 AgeMaleSkills 字段均是如此。

以上是在 JSON 结构已知情况下的解码,如果 JSON 结构是动态的、未知的,又该怎么处理呢?学院君将在下一篇教程中与大家探讨这个问题。

上一篇: RPC 编程(三):引入 jsonrpc 包通过 JSON 对 RPC 传输数据进行编解码

下一篇: JSON 处理篇(下):未知结构 JSON 数据解码和 JSON 流式读写实现