Go:nil切片的JSON序列化

【译文】原文地址
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()方法的情况,可以很好的工作。

你可能感兴趣的:(Go:nil切片的JSON序列化)