如何控制 Go 編碼 JSON 資料時的行為

originator發表於2020-02-25

今天來聊一下我在Go中對資料進行 JSON 編碼時遇到次數最多的三個問題以及解決方法,大家來看看是不是也為這些問題撓掉了不少頭髮。

自定義JSON鍵名

這個問題加到文章裡我是有所猶豫的,因為基本上大家都會,不過屬於同類問題我還是放進來了,對新接觸Go 的同學更友好些。

我們先從最常見的一個問題說,首先在Go 程式中要將資料編碼成JSON 格式時通常我們會先定義結構體型別,將資料存放到結構體變數中。

type Address struct {
    Type    string
    City    string  
    Country string
}

type CreditCard struct {
    FirstName string
    LastName  string
    Addresses []*Address
    Remark    string
}

home := &Address{"private", "Aartselaar", "Belgium"}
office := &Address{"work", "Boom", "Belgium"}
card := VCard{"Jan", "Kersschot", []*Address{home, office}, "none"}

js, err := json.Marshal(card)
fmt.Printf("JSON format: %s", js)

只有匯出的結構體成員才會被編碼,這也就是我們為什麼選擇用大寫字母開頭的欄位名稱。在編碼時,預設使用結構體欄位的名字作為JSON物件中的key,但是一般JSON 是給HTTP介面返回資料使用的,在介面的規範裡針對資料我們一般都要求返回snake case風格的欄位名。解決這個問題的方法是在結構體宣告時在結構體欄位標籤裡可以自定義對應的JSON key

所以我們把結構體宣告改為如下即可:

type Address struct {
    Type    string  `json:"type"`
    City    string  `json:"city"`
    Country string  `json:"country"`
}

編碼JSON時忽略掉指定欄位

並不是所有資料我們都期望編碼到JSON中暴露給外部介面的,所以針對一些敏感的欄位我們往往希望將其從編碼後的JSON資料中忽略掉。那麼上面也說了只有匯出的結構體成員才會被編碼,有的同學會問我直接用小寫的欄位名不行嗎?可是未匯出欄位只能在包內訪問,像這種攜帶內部敏感資料的往往都是應用的基礎資料,由專案的公共包來提供的。那麼怎麼既能維持欄位的匯出性又能讓其在JSON資料中被忽略掉呢? 還是使用結構體的標籤進行註解,比如下面定義的結構體,可以把身份證IdCard欄位在JSON資料中去掉:

type User struct {
    Name    string  `json:"name"`
    Age     Int     `json:"int"`
    IdCard  string  `json:"-"`
}

encoding/json的原始碼中和文件中都列舉了通過結構體欄位標籤控制資料JSON編碼行為的說明:

// Field is ignored by this package.
Field int `json:"-"`

// Field appears in JSON as key "myName".
Field int `json:"myName"`

// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`

// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`

omitempty這個是欄位的資料為空時,在JSON中省略這個欄位。為的是節省資料空間,Protobuf編譯器生成的結構體程式碼中每個欄位標籤中都有omitempty。但是在Api開發中這個不常用,因為欄位不固定對前端很不友好。

Protobuf不瞭解的可以看我之前寫的文章《Protobuf語言指南》。

結構體欄位標籤的json註解中都不加omitempty後還遇到一種情況,就是資料型別為切片的欄位在資料為空的時候會被JSON編碼為null而不是[]。這個前端經常會問我沒資料的時候能不能不要返回null,每回還要多寫一個判斷。我的說辭都是不能,其實規範點講是應該返回[]的知識我是我自己沒找到到解決方法。作為一個在寫程式碼上有強迫症的人,這個問題還是想搞明白的,好在有一天在StackOverflow上看到一個答案,才發現是編碼的疏忽導致的。

解決空切片在JSON裡被編碼成null

因為切片的零值為nil,無指向記憶體的地址,所以當以這種形式定義var f []int初始化slice後,在JSON中將其編碼為null,如果想在 JSON 中將空 slice 編碼為[]則需用make初始化 slice為其分配記憶體地址:

執行下面的例子可以看出兩點的區別:

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Friends []string
}

func main() {
    var f1 []string
    f2 := make([]string, 0)

    json1, _ := json.Marshal(Person{f1})
    json2, _ := json.Marshal(Person{f2})

    fmt.Printf("%s\n", json1)
    fmt.Printf("%s\n", json2)
}

輸出:

{"Friends":null}
{"Friends":[]}

其實導致這個問題的原因是Go的append函式(甩鍋),我們都知道引用型別的變數定義後如果沒初始化他們的值是nil,無指向記憶體的地址,是無法直接使用的。但是append函式在給切片追加元素時會判斷切片是否已初始化,沒有的話會幫其初始化分配底層陣列。我的習慣是先宣告切片,然後再在下面的迴圈程式碼中向切片追加元素。但是如果迴圈沒有執行,比如你從資料庫沒查出資料,就會導致對應切片欄位在無資料時返回的是nil然後被JSON編碼成了null。所以這個算是一個經驗總結出來的Tip吧在寫程式碼時大家一定要注意了。

這就是我在開發時把資料編碼成JSON格式時遇到的三個問題和相應的解決方法。加上之前寫的解析JSON的文章,兩個文章加起來差不多就能彙總日常開發中關於encoding/json庫使用的各種問題了。

掃碼下方二維碼關注公眾號第一時間獲取有價值的技術原創文章。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

公眾號:網管叨bi叨 | Golang、PHP、Laravel、Docker等學習經驗分享

相關文章