如何控制 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 庫使用的各種問題了。

原文地址:https://zhuanlan.zhihu.com/p/104757141

更多原創文章乾貨分享,請關注公眾號
  • 如何控制 Go 編碼 JSON 資料時的行為
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章