Go:nil切片的JSON序列化

【譯文】原文地址
Go最令人沮喪的事之一是它在編碼JSON的時候如何對nil切片的處理胰柑。其并不是返回我們期望的空數(shù)組福澡,而是返回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}

這是根據(jù)json包對nil切片處理方式?jīng)Q定的:

數(shù)組和切片值編碼為JSON數(shù)組,除了[]byte編碼為base64字符串变隔,nil切片編碼為null JSON值。

有一些建議可以修改json包來處理nil切片:

  • encoding/json nilasempty將nil-slice編碼為[]
  • encoding/json: "nonil"結(jié)構(gòu)體標(biāo)簽將空切片映射為non-null
    目前這些建議還沒有在json包中實現(xiàn)。因此,為了解決null數(shù)組的問題,必須將nil切片設(shè)置為空切片碧囊。看如下修改:

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切片的地方都要這樣設(shè)置為空切片是很繁瑣的糯而。有沒有更好的方法?

方法1:自定義Marshaler

根據(jù)Go json文檔:

Marshal遞歸地遍歷值v泊窘。如果遇到的值實現(xiàn)了Marshaler接口并且不是空指針熄驼,Marshal函數(shù)會調(diào)用對應(yīng)類型的MarshalJSON方法來編碼JSON像寒。

因此,如果我們實現(xiàn)了Marshaler接口:

// Marshaler is the interface implemented by types that
// can marshal themselves into valid JSON.
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

對象在編碼成JSON的時候瓜贾,自定義的MarshalJSON方法會被調(diào)用诺祸。看如下修改:

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類型別名是必須的祭芦,為了防止調(diào)用json.Marshal時候陷入無限循環(huán)筷笨。

方法2:動態(tài)初始化

另一種處理nil切片的方法是使用reflect包動態(tài)地檢查結(jié)構(gòu)體中的每個字段,如果是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":[]}

總結(jié)

使用自定義Marshaler的缺點(diǎn)是必須為每個包含切片的結(jié)構(gòu)體實現(xiàn)Marshaler接口胃夏。動態(tài)初始化方式肯定會慢一些,因為需要對結(jié)構(gòu)體的每個字段都進(jìn)行檢查昌跌。然而這種方法仰禀,在有許多帶切片的結(jié)構(gòu)體,并且需要調(diào)用json.Marshal()方法的情況蚕愤,可以很好的工作答恶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市审胸,隨后出現(xiàn)的幾起案子亥宿,更是在濱河造成了極大的恐慌卸勺,老刑警劉巖砂沛,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異曙求,居然都是意外死亡碍庵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門悟狱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來静浴,“玉大人,你說我怎么就攤上這事挤渐∑幌恚” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵浴麻,是天一觀的道長得问。 經(jīng)常有香客問我,道長软免,這世上最難降的妖魔是什么宫纬? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮膏萧,結(jié)果婚禮上漓骚,老公的妹妹穿的比我還像新娘蝌衔。我一直安慰自己,他們只是感情好蝌蹂,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布噩斟。 她就那樣靜靜地躺著,像睡著了一般孤个。 火紅的嫁衣襯著肌膚如雪亩冬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天硼身,我揣著相機(jī)與錄音硅急,去河邊找鬼。 笑死佳遂,一個胖子當(dāng)著我的面吹牛营袜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丑罪,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼荚板,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吩屹?” 一聲冷哼從身側(cè)響起跪另,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎煤搜,沒想到半個月后免绿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡擦盾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年嘲驾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迹卢。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡辽故,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腐碱,到底是詐尸還是另有隱情誊垢,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布症见,位于F島的核電站喂走,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏筒饰。R本人自食惡果不足惜缴啡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瓷们。 院中可真熱鬧业栅,春花似錦秒咐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至帮孔,卻和暖如春雷滋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背文兢。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工晤斩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姆坚。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓澳泵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兼呵。 傳聞我的和親對象是個殘疾皇子兔辅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359