「每一个程序员都无法逃脱 JSON 的命运魔爪」
JSON 简直就是一个神奇的玩意,只要是人类可以阅读的数据结构,基本都可以转成 JSON 的数据格式,其在各个平台、组件、模块中穿梭不止,使用上更是游刃有余。甚至在 HTTP 接口上,有取代 FormData 的趋势(上传文件还是得 Form),成为 POST 数据的新宠儿。在这里我们需要感谢 Javascript,感谢前端工程师。
数据类型 | JSON | Golang |
字串 | string | string |
整数 | number | int64 |
浮点数 | number | flaot64 |
数组 | arrary | slice |
对象 | object | struct |
布尔 | bool | bool |
空值 | null | nil |
JSON 数据类型对照表
下面我将为大家呈现 JSON 在 Golang 中的应用。
一、 入门
Golang 中提供了 json 的标准库,可以完成有关 JSON 的数据转换功能。
json.Marshal
Marshal 大法好,可以把一切 Struct 抹成 JSON , 把每个 Struct 收拾的服服帖帖,毫无退路可言。
在 Struct 中可以通过 json Tag 来给 JSON 转换的结果指定键值:
code:type People struct { Name string `json:"nickname"` Age int64 `json:"age"`}func main(){ j := People{ Name : "Haha", Age : 18, } jsonByte, _ := json.MarshalIndent(j, "" ," ") fmt.Println(string(jsonByte))}output:{ "nickname": "Haha", "age": 18}
你可能注意到,我并没有使用 json.Marshal 方法,而是使用了 json.MarshalIntent 方法。
json.MarshalIntent 可以称之为 json.Marshal 的漂亮版,简称 「马漂亮」,是用来打日志的一把好手。
json.Unmarshal
正如其名,此方法为将 JSON 的字节,转换成具体指定的数据结构。我们来试验一下。
接上 code:m := make(map[string]interface{})_ = json.Unmarshal(jsonByte, m)fmt.Println(m)output:map[]%
好像没有得到预期的答案?对了, json.Unmarshal 的第二个参数需要是指针类型才行,由于 Golang 中所有参数都是值传递,原始参数不能被函数内部所修改。
接上 code:m := make(map[string]interface{})_ = json.Unmarshal(jsonByte, &m)fmt.Println(m)output:map[age:18 nickname:Haha]
我们如期的得到了转化后的值,是一个 map 类型, key 为 Struct json tag 指定的 key , 值为具体的 JSON 对应数值。
但是这里有个陷阱, json.Unmarshal 默认会把所有的数字都转化为 float64 类型,而不是 int64 类型。不信的话可以测试一下:
fmt.Println(m["age"].(int64))output:panic: interface conversion: interface {} is float64, not int64
至此我们得到了一个痛的领悟。
所以如果我们想要得到一个 int64 类型的值,则需要强制类型转换一下:
fmt.Println(int64(m["age"].(float64)))output:18
不过除了强制类型转换,还有另外一个方式来解决此问题。
json.NewDecoder
json.NewDecoder 可以通过读取 io.Reader 类型的参数来直接使用 stream 数据,在获取 HTTP POST 数据的时候使用会特别的舒爽。但是如果已知 []byte 二进制数组的话,建议还是直接使用 json.Marshal 方法比较简单。
json.NewDecoder 返回了 Decoder 结构体实例,此结构体有一个叫 UseNumber 的方法,此方法可以定义在转换过程中,将 JSON 的数字转换为 json.Number 类型,而 json.Number 又有 Int64 / Float64 / String 三个方法,可以将值优雅地处理为你想要的结果。
改造上面的代码,变为:
code:j := People{ Name : "Haha", Age : 18,}jsonByte, _ := json.MarshalIndent(j, "" ," ")jDecoder := json.NewDecoder(strings.NewReader( string(jsonByte)))m := make(map[string]interface{})jDecoder.UseNumber()_ = jDecoder.Decode(&m)fmt.Println(m["age"].(json.Number).Int64())output:18 <nil>
这样看,是不是更优雅了一些,更「Golang」了一些。
其实 json.Number 里的三个类型转换方法做的事情超级简单,就是调用了 系统提供的 strconv 中的一些方法来实现类型转换,部分代码如下:
pakcage json// A Number represents a JSON number literal.type Number string// String returns the literal text of the number.func (n Number) String() string { return string(n) }// Float64 returns the number as a float64.func (n Number) Float64() (float64, error) { return strconv.ParseFloat(string(n), 64)}// Int64 returns the number as an int64.func (n Number) Int64() (int64, error) { return strconv.ParseInt(string(n), 10, 64)}
二、升华
在项目中,难免会遇到坑,优秀的程序员会在坑里躺一会儿,就能想到填坑的办法。
- 选择性导出
对,没有看错,是「-」,在 json 的标签中添加 - ,而不是具体的键值,由此在 json.Marshal 后,将不再导出此键值。这样的话,可以处理在 Struct 中可以导出(不可导出的成员同时也不能被 json.Marshal 导出),但是 JSON 结果中不可导出的尴尬情景。
code:type People struct { Name string `json:"nickname"` Age int64 `json:"age"` Gender string `json:"-"`}output:{ "nickname": "Haha", "age": 18}
omitempty 空值不导出
如果你有幸遇到十分苛刻有洁癖的前端工程师,他们不希望你返回「null」值,如果字段是 null,应该直接不返回该字段。除了自己写个递归把接口层返回值都筛选一遍外,还可以在 Struct json tag 里加入「omitempty」佐料。这样就标记了在 json.Marshal 的时候,如果此值为 null ,自动不导出此值,比如
code:type People struct { Name string `json:"nickname"` Age int64 `json:"age"` Gender string `json:"-"` Body interface{} `json:"body,omitempty"`}cond 1:j := People{ Name : "Haha", Age : 18, Body: "waaaaa",}output:{ "nickname": "Haha", "age": 18, "body": "waaaaa"}cond 2:j := People{ Name : "Haha", Age : 18,}output:{ "nickname": "Haha", "age": 18}而不是{ "nickname": "Haha", "age": 18, "body": null}
空值也导出
但是也有一些时候,你需要导出空值,但是是对应类型的空值,而不是 null。比如空数组是 [] , 空对象是 {}。那么在导出的时候,需要确认对应的值已经被正确的初始化并分配内存。比如空切片,
var address []int64
的 JSON 导出为 null,但是
address := []int64{}
的 JSON 导出为 []。这两个的区别就是第二个语句为变量分配了内存空间,不过仍然是一个空的切片。而第一个语句只是一个定义。
再比如 map
var body map[string]string
的 JSON 导出为 null, 但是
body := make(map[string]string)
的 JSON 导出为 {} 。
三、飞天
MarshalJSON 和 UnmarshalJSON 自定义转换结果
在更变态的需求中,可能需要 Struct 直接自定义一个 JSON 返回结构,而不是导出自己的成员变量。这时候就要祭出 json 大杀器,MarshalJSON 和 UnmarshalJSON。
这一对方法,可以直接返回 json 的转换结构,忽略结构体本来的面目。
code:type People struct { Name string `json:"nickname"` Age int64 `json:"age"` Gender string `json:"-"` Body interface{} `json:"body,omitempty"` Address []string `json:"address"`}func (p People) MarshalJSON() ([]byte, error){ return []byte("{\"a\": 123} "), nil}func (p *People) UnmarshalJSON(data []byte) error{ *p = People{ Name: "Forbidden", } return nil}j := People{ Name : "Haha", Age : 18,}jsonByte, _ := json.MarshalIndent(j, "" ," ")fmt.Println(string(jsonByte))m := People{}_ = json.Unmarshal(jsonByte, &m)fmt.Println(m)output:{ "a": 123}{Forbidden 0 <nil> []}
通过 MarshalJSON 和 UnmarhsalJSON,就可以直接控制 JSON 转换的结果,如庖丁解牛,一丝不挂。
不过需要注意的是, MarshalJSON 方法是非指针的,而 UnmarshalJSON 方法是指针类型的。
json.RawMessage 延时转换
RawMessage 可以更细粒度的控制解码节奏,如果一个 Struct 成员被设置为 json.RawMessage 类型,那么它就需要被单独 Unmarshal 才行,否则它一直保持二进制的样子,比如:
type People struct { Name string `json:"nickname"` Age int64 `json:"age"` Address json.RawMessage `json:"address"` }address, _ := json.Marshal("加利福尼亚")j := People{ Name : "Haha", Age : 18, Address: address,}jsonByte, _ := json.MarshalIndent(j, "" ," ")fmt.Println(string(jsonByte))m := People{}_ = json.Unmarshal(jsonByte, &m)fmt.Println(m)var any interface{}_ = json.Unmarshal(m.Address, &any)fmt.Println(any)output:{ "nickname": "Haha", "age": 18, "address": "加利福尼亚"}{Haha 18 [34 229 138 160 229 136 169 231 166 143 229 176 188 228 186 154 34]}加利福尼亚
如果不单独解码,则 Address 一直是 []byte 的数据格式。除非单独对其进行 Unmashal 解码。
四、总结
优雅地处理 JSON,能让后端对你刮目相看,更能和前端和谐共处,让世界变得更美好。