0. 前言
目前我们看到很多的开放平台,基本上都是采用了JSON作为他们的数据交互的格式。既然JSON在Web开发中如此重要,那么Go语言对JSON支持的怎么样呢?Go语言的标准库已经非常好的支持了JSON,可以很容易的对JSON数据进行编、解码的工作。
如果有更灵活的需求也有不错的第三方库提供支持。
这篇文章将全面解读Golang中JSON的使用。
1. JSON简介
JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读。尽管JSON是Javascript Standard ECMA-262 3rd Edition – December 1999的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯, 这些特性使JSON成为理想的数据交换语言。
JSON与XML最大的不同在于XML是一个完整的标记语言,而JSON相当于一个阉割版,所以JSON由于比XML更小、更快,更易解析,以及浏览器的内建快速解析支持,使得其更适用于网络数据传输领域。
在讲解JSON的数据结构之前, 我们先来一段简单的样例JSON数据:
[
{
"precision": "zip",
"Latitude": 37.7668,
"Longitude": -122.3959,
"Address": "",
"City": "SAN FRANCISCO",
"State": "CA",
"Zip": "94107",
"Country": "US"
},
{
"precision": "zip",
"Latitude": 37.371991,
"Longitude": -122.026020,
"Address": "",
"City": "SUNNYVALE",
"State": "CA",
"Zip": "94085",
"Country": "US"
}
]
JSON建构于两种结构:
- 键值对的集合(A collection of name/value pairs): 在不同的语言中, 他们被理解为: object(Javascript), struct(Golang), Dictinary(Python), 以及哈希表(hash table), 有键列表(keyed list), 或者关联数组(associative array).
- 值的有序列表(An ordered list of values): 在大部分语言中,它被理解为数组(array).
其中值可以包含如下类型, 并且这些结构可以嵌套:
- 字符串(string): 由双引号包围的任意数量Unicode字符的集合,使用反斜线转义, 一个字符(character)即一个单独的字符串(character string)
- 数值(number): 同时包含整数和浮点数
- 布尔值(booleans): 布尔值包含: true和false
- 空(null): 空 对象(object): 键值对的集合
- 数组(array): 值的有序列表
这些都是常见的数据结构。事实上大部分现代计算机语言都以某种形式支持它们。这使得一种数据格式在同样基于这些结构的编程语言之间交换成为可能。
2. JSON与Go数据结构映射
JSON格式可以算我们日常最常用的序列化格式之一了, Go语言作为一个由Google开发, 号称互联网的C语言, 自然也对JSON格式支持很好。Golang的 encoding/json标准库 实现的JSON标准(RFC 4627)的编码和解码, 可以让我们很方便地进行JSON数据的转换.
具体详情可以参考标准库 Marshal 和 Unmarshal 函数的注释, 下面是一个基本的数据关系映射总结:
Golang Type | JSON Type | Description |
---|---|---|
bool | booleans | true / false |
int float | numbers | 对应 golang 所有的数值类型 |
string | strings | 字符串会转换成 UTF-8 ,无法转换的会打印对应的 Unicode 编码。 为了防止浏览器把 json 输出当作 html, 其中 “<”、”>”、”&” 会被转义为 “\u003c”、”\u003e”、”\u0026” |
array \r slice | arrays | Publish Acknowledgment |
struct | objects | 只有导出的字段(以大写字母开头)才会输出中 |
nil | null | 空 |
Go语言是个强类型语言,对格式要求极其严格而JSON格式虽然也有类型,但是并不稳定,Go语言在解析来源为非强类型语言时比如PHP,Python等序列化的JSON时,经常遇到一些问题诸如字段类型变化导致无法正常解析的情况,导致服务不稳定。所以在做JSON相关解码和编码的过程中, 需要注意以下事项:
- Go语言中一些特殊的类型,比如 Channel、complex、function 是不能被解析成JSON的.
- JSON对象的
key 必须是 string
,所以要编码一个 map ,那么必须是map[string]interface{} (interface{}是Go语言中任意的类型) - 嵌套的数据是不能编码,不然会让JSON编码进入死循环
- 指针在编码的时候会输出指针指向的内容,而空指针会输出null
3. 标准库解读
在使用标准库进行 json 操作之前, 先简单了解下标准库提供了那些对JSON的操作, 以下解读主要来源于GoDoc
3.1 函数
Compact: 用于JSON字符串的拼接, 拼接时会校验后面的字符串是否是合法json, 如果不是会报错, 但对字符串中的特殊字符(html不安全字符,比如上面提到的”<” “>”等)不进行转义.
HTMLEscape: 和Compact相对, 拼接JSON字符串时会进行特殊字符转义, 转义成web安全的字符.
Valid: 校验数据是否是合法的JSON编码数据, 往往用于数据格式校验.
Marshal: 用于编码JSON.
Indent: 用于JSON的格式化输出, 最常见的用法是定义JSON的缩进,比如2个空格的缩进.
MarshalIndent: 编码JSON后,带格式化的输出.
Unmarshal: 用于解码JSON.
3.2 接口
Unmarshaler: 用于自定义解码json方法
Marshaler: 用于自定义编码json的方法
3.3 结构体
Decoder: 从一个输入流读取数据并且解析json
Encoder: 把一个编码后的json值写出给一个输出流
Number: JSON里面的number类型
RawMessage: 是一种已经被编码的json字符串, 它实现了Marshaler和Unmarshaler, 可以用来延迟解析部分json
Token: 一个空interface, 持有一种Json映射的Go内部数据结构的值
Delim, for the four JSON delimiters [ ] { }
bool, for JSON booleans
float64, for JSON numbers
Number, for JSON numbers
string, for JSON string literals
nil, for JSON null
3.4 异常
InvalidUTF8Error: 用于兼容golang1.2版本之前, 1.2过后不会有该异常
InvalidUnmarshalError: 表示解码json时传递了一个非法的参数, 比如一个空指针
MarshalerError: Marshaler异常
SyntaxError: json的语法错误
UnmarshalFieldError: 降级, 未使用, 为了兼容保留
UnmarshalTypeError: 解码时 遇到不认识的json类型, 表明传入的json的类型无法被转换成Golang对应的类型, 比如JSON RFC增加新的JSON类型 就会遇到这样的错误
UnsupportedTypeError: 编码时 遇到不认识的Golang类型, 不知道该Golang的数据类型应该被映射成那种json类型, 比如自定义的类型(未实现 marshaler接口)
UnsupportedValueError: 同上, 遇到不认识的json类型, 比如 你需要将golang里面的”a”编程成json里面不存在的类型
4. Struct Tag
在JSON的解析过程中Struct Tag被频繁使用, 因此在进行真正的解析之前, 介绍下Golang中的Struct Tag,在golang中, 命名都是推荐都是用驼峰方式, 并且在首字母大小写有特殊的语法含义(大写变量可以导出包, 小写变量包私有)。但是由于经常需要和其它的系统进行数据交互, 例如转成json格式, 存储到mongodb啊等等。这个时候如果用属性名来作为键值可能不一定会符合项目要求, 比如不是用Struct Tag时, JSON解析出来的结果是这样的:
package main
import (
"encoding/json"
"fmt"
)
// User is test for json
type User struct {
ID string
Name string
}
func main() {
u := User{ID: "user001", Name: "tom"}
jsonU, _ := json.Marshal(u)
fmt.Println(string(jsonU))
}
输出内容如下:
{"ID":"user001","Name":"tom"}
显然如果这样解析JSON会太死板, 无法面对灵活的业务, 而具体如何转换应该交给我们自己控制, 而Struct Tag就是用来干这个事儿的。
Struct Tag 采用跟随在 Struct Field 后面。
那 Struct Tag 的工作原理是咋样的? 需要用到Tag中的内容时, 咋样去获取喃? 其实是使用反射(reflect)中的方法来获取的:
package main
import (
"fmt"
"reflect"
)
// User is test for json
type User struct {
ID string `json:"json_id" bson:"bson_id" custom:"my_id"`
Name string `json:"json_name" bson:"bson_name" custom:"my_name"`
}
func main() {
u := &User{ID: "user001", Name: "tom"}
t := reflect.TypeOf(u)
// 获取第一个字段的Struct Tag的值
f0 := t.Elem().Field(0)
fmt.Println(f0.Tag.Get("json"))
fmt.Println(f0.Tag.Get("bson"))
fmt.Println(f0.Tag.Get("custom"))
}
输出结果如下:
json_id
bson_id
my_id
解析JSON:
通过标准库提供的 Unmarshal 函数来解析JSON, 但是标准库在解析未知格式的JSON时比较麻烦, 需要解析到interface{},然后断言, 因此如果想要灵活的解析JSON可以使用一些第三方库,比如jsonitor
5.1 解析已知JSON
之前介绍了Golang中的Struct Tag, 而标准库encoding/json就是利用Stuct Tag可以轻松实现JSON编解码过程中的一些自定义转换, 而关于JSON Struct Tag具体的值, 标准库文档里面有相应的描述, 这里作简单的概述:
Json Struct Tag 格式为json: “filed_name,argument”
filed_name 为用户自定义的需要转换的字段名, 如果为”-“表示 转换时直接忽略字段
argument 表示该字段转换时的一些额外的参数
omitempty 表示如果为空置则忽略字段
json数据类型, 比如string, numbers, 表示在转换时, 调整成对应的数据类型
package main
import (
"encoding/json"
"fmt"
)
// Product _
type Product struct {
Name string `json:"name"`
ProductID int64 `json:"product_id,string"`
Number int `json:"number,string"`
Price float64 `json:"price,string"`
IsOnSale bool `json:"is_on_sale,string"`
Test string `json:"-"`
OMTest string `json:"om_test,omitempty"`
}
func main() {
str := `{"name":"test","product_id":"1","number":"110011","price":"0.01","is_on_sale":"true"}`
p := Product{}
json.Unmarshal([]byte(str), &p)
fmt.Println(p)
}
输出结果:
{test 1 110011 0.01 true }
5.2 解析未知JSON
上面那种解析方式是在我们知晓被解析的JSON数据的结构的前提下采取的方案, 如果我们不知道被解析的数据的格式, 又应该如何来解析呢? 我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。
- 解析到interface{}
package main import ( "encoding/json" "fmt" ) // Product _ type Product struct { Name string `json:"name"` ProductID int64 `json:"product_id,string"` Number int `json:"number,string"` Price float64 `json:"price,string"` IsOnSale bool `json:"is_on_sale,string"` Test string `json:"-"` OMTest string `json:"om_test,omitempty"` } func main() { // 假设我们并不知道这个JSON的格式, 我们可以将他解析到interface{} str := `{"name":"test","product_id":"1","number":"110011","price":"0.01","is_on_sale":"true"}` var p interface{} json.Unmarshal([]byte(str), &p) // 现在我们需要从这个interface{}解析出里面的数据 m := p.(map[string]interface{}) for k, v := range m { switch vv := v.(type) { case string: fmt.Printf("%s is string, value: %s\n", k, vv) case int: fmt.Printf("%s is int, value: %d\n", k, vv) case int64: fmt.Printf("%s is int64, value: %d\n", k, vv) case bool: fmt.Printf("%s is bool, vaule: %v", k, vv) default: fmt.Printf("%s is unknow type\n", k) } } }
输出结果:
name is string, value: test
product_id is string, value: 1
number is string, value: 110011
price is string, value: 0.01
is_on_sale is string, value: true
- 使用第三方库jsonitor进行解析
大量的类型断言是不是让你觉得很烦, 如果是多层interface{}嵌套那么断言需要更多, 因此就有很多第三方JSON解析库出现, 他们尽量采用流式迭代解析, 这里我用过的比较不错的是陶文的jsonitor:
package main
import (
"fmt"
"github.com/json-iterator/go"
)
// Product _
type Product struct {
Name string `json:"name"`
ProductID int64 `json:"product_id,string"`
Number int `json:"number,string"`
Price float64 `json:"price,string"`
IsOnSale bool `json:"is_on_sale,string"`
Test string `json:"-"`
OMTest string `json:"om_test,omitempty"`
}
func main() {
str := `{"name":"test","product_id":"1","number":"110011","price":"0.01","is_on_sale":"true"}`
fmt.Println(jsoniter.Get([]byte(str), "price").ToFloat64())
}
5.3 自定义解析
struct实现Unmarshaler接口, 便可以实现解析JSON的过程,
package main
import (
"encoding/json"
"fmt"
)
// Product _
type Product struct {
Name string `json:"name"`
ProductID int64 `json:"product_id,string"`
Number int `json:"number,string"`
Price float64 `json:"price,string"`
IsOnSale bool `json:"is_on_sale,string"`
Test string `json:"-"`
OMTest string `json:"om_test,omitempty"`
}
// UnmarshalJSON 自定义解析
func (p *Product) UnmarshalJSON(data []byte) error {
// 示例代码使用jsonitor代为解析
p.Price = 0.01
p.Number = 1100
p.Name = "my_test_name"
return nil
}
// MarshalJSON 自定义编码
func (p *Product) MarshalJSON() ([]byte, error) {
// 自己编码json
return []byte(`{"test":"name_test"}`), nil
}
func main() {
str := `{"name":"test","product_id":"1","number":"110011","price":"0.01","is_on_sale":"true"}`
p := Product{}
json.Unmarshal([]byte(str), &p)
fmt.Println(p)
}
6. 生成JSON
我们可以通过标准库json将Struc序列化成JSON也可以自定义序列化的方法
6.1 通过struct生成JSON
上面在介绍JSON解析的时候已经介绍了关于JSON的Struct Tag了, 因此直接参考代码:
package main
import (
"encoding/json"
"fmt"
)
// Product _
type Product struct {
Name string `json:"name"`
ProductID int64 `json:"product_id,string"`
Number int `json:"number,string"`
Price float64 `json:"price,string"`
IsOnSale bool `json:"is_on_sale,string"`
Test string `json:"-"`
OMTest string `json:"om_test,omitempty"`
}
func main() {
p := &Product{
Name: "test",
ProductID: 01,
Number: 110011,
Price: 0.01,
IsOnSale: true,
Test: "test",
}
jsonP, _ := json.Marshal(p)
fmt.Println(string(jsonP))
}
输出结果如下:
{"name":"test","product_id":"1","number":"110011","price":"0.01","is_on_sale":"true"}
6.2 自定义生成
Struct实现Marshaler接口, 便可以自定义编码过程
package main
import (
"encoding/json"
"fmt"
)
// Product _
type Product struct {
Name string `json:"name"`
ProductID int64 `json:"product_id,string"`
Number int `json:"number,string"`
Price float64 `json:"price,string"`
IsOnSale bool `json:"is_on_sale,string"`
Test string `json:"-"`
OMTest string `json:"om_test,omitempty"`
}
// UnmarshalJSON 自定义解析
func (p *Product) UnmarshalJSON(data []byte) error {
// 示例代码使用jsonitor代为解析
p.Price = 0.01
p.Number = 1100
p.Name = "my_test_name"
return nil
}
// MarshalJSON 自定义编码
func (p *Product) MarshalJSON() ([]byte, error) {
// 自己编码json
return []byte(`{"test":"name_test"}`), nil
}
func main() {
p := &Product{
Name: "test",
ProductID: 01,
Number: 110011,
Price: 0.01,
IsOnSale: true,
Test: "test",
}
jsonP, _ := json.Marshal(p)
fmt.Println(string(jsonP))
}
作者:Golang发烧友
链接:https://zhuanlan.zhihu.com/p/393998886