【译文】原文地址
Go最令人沮丧的事之一是它在编码JSON的时候如何对nil切片的处理。其并不是返回我们期望的空数组,而是返回Null,如下代码所示:
package main
import (
"encoding/json"
"fmt"
)
// Bag holds items
type Bag struct {
Items []string
}
// PrintJSON converts payload to JSON and prints it
func PrintJSON(payload interface{}) {
response, _ := json.Marshal(payload)
fmt.Printf("%s\n", response)
}
func main() {
bag1 := Bag{}
PrintJSON(bag1)
}
Outputs:
{"Items":null}
这是根据json包对nil切片处理方式决定的:
数组和切片值编码为JSON数组,除了[]byte编码为base64字符串,nil切片编码为null JSON值。
有一些建议可以修改json包来处理nil切片:
- encoding/json nilasempty将nil-slice编码为[]
- encoding/json: "nonil"结构体标签将空切片映射为non-null
目前这些建议还没有在json包中实现。因此,为了解决null数组的问题,必须将nil切片设置为空切片。看如下修改:
package main
import (
"encoding/json"
"fmt"
)
// Bag holds items
type Bag struct {
Items []string
}
// PrintJSON converts payload to JSON and prints it
func PrintJSON(payload interface{}) {
response, _ := json.Marshal(payload)
fmt.Printf("%s\n", response)
}
func main() {
bag1 := Bag{}
bag1.Items = make([]string, 0)
PrintJSON(bag1)
}
输出更改为:
{"Items":[]}
然而,在任何可能存在nil切片的地方都要这样设置为空切片是很繁琐的。有没有更好的方法?
方法1:自定义Marshaler
根据Go json文档:
Marshal递归地遍历值v。如果遇到的值实现了Marshaler接口并且不是空指针,Marshal函数会调用对应类型的MarshalJSON方法来编码JSON。
因此,如果我们实现了Marshaler接口:
// Marshaler is the interface implemented by types that
// can marshal themselves into valid JSON.
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
对象在编码成JSON的时候,自定义的MarshalJSON方法会被调用。看如下修改:
package main
import (
"encoding/json"
"fmt"
)
// Bag holds items
type Bag struct {
Items []string
}
// MarshalJSON initializes nil slices and then marshals the bag to JSON
func (b Bag) MarshalJSON() ([]byte, error) {
type Alias Bag
a := struct {
Alias
}{
Alias: (Alias)(b),
}
if a.Items == nil {
a.Items = make([]string, 0)
}
return json.Marshal(a)
}
// PrintJSON converts payload to JSON and prints it
func PrintJSON(payload interface{}) {
response, _ := json.Marshal(payload)
fmt.Printf("%s\n", response)
}
func main() {
bag1 := Bag{}
PrintJSON(bag1)
}
Outputs:
{"Items":[]}
代码中Alias类型别名是必须的,为了防止调用json.Marshal时候陷入无限循环。
方法2:动态初始化
另一种处理nil切片的方法是使用reflect包动态地检查结构体中的每个字段,如果是nil切片就用空切片来替换。如下所示:
package main
import (
"encoding/json"
"fmt"
"reflect"
)
// Bag holds items
type Bag struct {
Items []string
}
// NilSliceToEmptySlice recursively sets nil slices to empty slices
func NilSliceToEmptySlice(inter interface{}) interface{} {
// original input that can't be modified
val := reflect.ValueOf(inter)
switch val.Kind() {
case reflect.Slice:
newSlice := reflect.MakeSlice(val.Type(), 0, val.Len())
if !val.IsZero() {
// iterate over each element in slice
for j := 0; j < val.Len(); j++ {
item := val.Index(j)
var newItem reflect.Value
switch item.Kind() {
case reflect.Struct:
// recursively handle nested struct
newItem = reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(item.Interface())))
default:
newItem = item
}
newSlice = reflect.Append(newSlice, newItem)
}
}
return newSlice.Interface()
case reflect.Struct:
// new struct that will be returned
newStruct := reflect.New(reflect.TypeOf(inter))
newVal := newStruct.Elem()
// iterate over input's fields
for i := 0; i < val.NumField(); i++ {
newValField := newVal.Field(i)
valField := val.Field(i)
switch valField.Kind() {
case reflect.Slice:
// recursively handle nested slice
newValField.Set(reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(valField.Interface()))))
case reflect.Struct:
// recursively handle nested struct
newValField.Set(reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(valField.Interface()))))
default:
newValField.Set(valField)
}
}
return newStruct.Interface()
case reflect.Map:
// new map to be returned
newMap := reflect.MakeMap(reflect.TypeOf(inter))
// iterate over every key value pair in input map
iter := val.MapRange()
for iter.Next() {
k := iter.Key()
v := iter.Value()
// recursively handle nested value
newV := reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(v.Interface())))
newMap.SetMapIndex(k, newV)
}
return newMap.Interface()
case reflect.Ptr:
// dereference pointer
return NilSliceToEmptySlice(val.Elem().Interface())
default:
return inter
}
}
// PrintJSON converts payload to JSON and prints it
func PrintJSON(payload interface{}) {
newPayload := NilSliceToEmptySlice(payload)
response, _ := json.Marshal(newPayload)
fmt.Printf("%s\n", response)
}
func main() {
bag1 := Bag{}
PrintJSON(bag1)
}
Output:
{"Items":[]}
总结
使用自定义Marshaler的缺点是必须为每个包含切片的结构体实现Marshaler接口。动态初始化方式肯定会慢一些,因为需要对结构体的每个字段都进行检查。然而这种方法,在有许多带切片的结构体,并且需要调用json.Marshal()方法的情况,可以很好的工作。