簡化 Go 中對 JSON 的處理

我的第一個 Go 工程需要處理一堆 JSON 測試固件並把 JSON 數據作爲參數傳給我們搭建的 API 處理。另一個團隊爲了給 API 提供語言無關的、可預期的輸入和輸出,創建了這些測試固件。

在強類型語言中,JSON 通常很難處理 —— JSON 類型有字符串、數字、字典和數組。如果你使用的語言是 javascript、python、ruby 或 PHP,那麼 JSON 有一個很大的好處就是在解析和編碼數據時你不需要考慮類型。

// in PHP
$object = json_decode('{"foo":"bar"}');

// in javascript
const object = JSON.parse('{"foo":"bar"}')

在強類型語言中,你需要自己去定義怎麼處理 JSON 對象的字符串、數字、字典和數組。在 Go 語言中,你使用內建的 API 時需要考慮如何更好地把一個 JSON 文件轉換成 Go 的數據結構。我不打算深入研究在 Go 中如何處理 JSON 這個複雜的話題,我只列出兩個代碼的例子來闡述下這個問題。源碼詳情請見 Go 實例教程 [1]

解析 / 序列化爲 map[string]interface

首先,來看這個程序

package main

import (
    "encoding/json"
    "fmt"
)


func main() {

    byt := []byte(`{
        "num":6.13,
        "strs":["a","b"],
        "obj":{"foo":{"bar":"zip","zap":6}}
    }`)
    var dat map[string]interface{}
    if err := json.Unmarshal(byt, &dat); err != nil {
        panic(err)
    }
    fmt.Println(dat)

    num := dat["num"].(float64)
    fmt.Println(num)

    strs := dat["strs"].([]interface{})
    str1 := strs[0].(string)
    fmt.Println(str1)

    obj := dat["obj"].(map[string]interface{})
    obj2 := obj["foo"].(map[string]interface{})
    fmt.Println(obj2)

}

我們把 JSON 數據從 byt 變量反序列化(如解析、解碼等等)成名爲 dat 的 map / 字典對象。這些操作跟其他語言類似,不同的是我們的輸入需要是字節數組(不是字符串),對於字典的每個值時需要有類型斷言 [2] 才能讀取或訪問該值。

當我們處理一個多層嵌套的 JSON 對象時,這些類型斷言會讓處理變得非常繁瑣。

解析 / 序列化爲 struct

第二種處理如下:

package main

import (
    "encoding/json"
    "fmt"
)

type ourData struct {
    Num   float64 `json:"num"`
    Strs []string `json:"strs"`
    Obj map[string]map[string]string `json:"obj"`
}

func main() {
    byt := []byte(`{
        "num":6.13,
        "strs":["a","b"],
        "obj":{"foo":{"bar":"zip","zap":6}}
    }`)

    res := ourData{}
    json.Unmarshal(byt, &res)
    fmt.Println(res.Num)
    fmt.Println(res.Strs)
    fmt.Println(res.Obj)
}

我們利用 Go struct 的標籤功能把 byt 變量中的字節反序列化成一個具體的結構 ourData。

標籤是結構體成員定義後跟隨的字符串。我們的定義如下:

type ourData struct {
    Num   float64 `json:"num"`
    Strs []string `json:"strs"`
    Obj map[string]map[string]string `json:"obj"`
}

你可以看到 Num 成員的 JSON 標籤 “num”、Str 成員的 JSON 標籤 “strs”、Obj 成員的 JSON 標籤 “obj”。這些字符串使用反引號 [3] 把標籤聲明爲文字串。除了反引號,你也可以使用雙引號,但是使用雙引號可能會需要一些額外的轉義,這樣看起來會很凌亂。

type ourData struct {
    Num   float64 "json:\"num\""
    Strs []string "json:\"strs\""
    Obj map[string]map[string]string "json:\"obj\""
}

在 struct 的定義中,標籤不是必需的。如果你的 struct 中包含了標籤,那麼它意味着 Go 的 反射 API[4] 可以訪問標籤的值 [5]。Go 中的包可以使用這些標籤來進行某些操作。

Go 的 encoding/json 包在反序列化 JSON 成員爲具體的 struct 時,通過這些標籤來決定每個頂層的 JSON 成員的值。換句話說,當你定義如下的 struct 時:

type ourData struct {
    Num   float64   `json:"num"`
}

意味着:

當使用 json.Unmarshal 反序列化 JSON 對象爲這個 struct 時,取它頂層的 num 成員的值並把它賦給這個 struct 的 Num 成員。

這個操作可以讓你的反序列化代碼稍微簡潔一點,因爲程序員不需要對每個成員取值時都顯式地調用類型斷言。然而,這個仍不是最佳解決方案。

首先 —— 標籤只對頂層的成員有效 —— 嵌套的 JSON 需要對應嵌套的類型(如 Obj map[string]map[string]string),因此繁瑣的操作仍沒有避免。

其次 —— 它假定你的 JSON 結構不會變化。如果你運行上面的程序,你會發現 "zap":6 並沒有被賦值到 Obj 成員。你可以通過創建類型 map[string]map[string]interface{} 來處理,但是在這裏你又需要進行類型斷言了。

這是我第一個 Go 工程遇到的情況,曾讓我苦不堪言。

幸運的是,現在我們有了更有效的辦法。

SJSON 和 GJSON

Go 內建的 JSON 處理並沒有變化,但是已經出現了一些成熟的旨在用起來更簡潔高效的處理 JSON 的包。

SJSON[6](寫 JSON)和 GJSON[7](讀 JSON)是 Josh Baker[8] 開發的兩個包,你可以用來讀寫 JSON 字符串。你可以參考 README 來獲取代碼實例 —— 下面是使用 GJSON 從 JSON 字符串中獲取嵌套的值的示例:

package main

import "github.com/tidwall/gjson"

const JSON = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`

func main() {
    value := gjson.Get(json, "name.last")
    println(value.String())
}

類似的,下面是使用 SJSON “設置” JSON 字符串中的值返回設置之後的字符串的示例代碼:

package main

import "github.com/tidwall/sjson"

const JSON = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`

func main() {
    value, _ := sjson.Set(json, "name.last""Anderson")
    println(value)
}

如果 SJSON 和 GJSON 不符合你的口味,還有一些 [9] 其他的 [10] 第三方庫 [11],可以用來在 Go 程序中稍微複雜點地處理 JSON。


via: https://alanstorm.com/simplified-json-handling-in-go/

作者:Alan[12] 譯者:lxbwolf[13] 校對:polaris[14]

本文由 GCTT[15] 原創編譯,Go 中文網 [16] 榮譽推出

參考資料

[1]

Go 實例教程: https://gobyexample.com/json

[2]

類型斷言: https://www.sohamkamani.com/golang/type-assertions-vs-type-conversions/

[3]

反引號: https://golangbyexample.com/double-single-back-quotes-go/

[4]

反射 API: https://pkg.go.dev/reflect

[5]

訪問標籤的值: https://stackoverflow.com/questions/23507033/get-struct-field-tag-using-go-reflect-package/23507821#23507821

[6]

SJSON: https://github.com/tidwall/sjson

[7]

GJSON: https://github.com/tidwall/gjson

[8]

Josh Baker: https://github.com/tidwall

[9]

一些: https://github.com/pquerna/ffjson

[10]

其他的: https://github.com/mailru/easyjson

[11]

第三方庫: https://github.com/Jeffail/gabs

[12]

Alan: https://alanstorm.com/about/

[13]

lxbwolf: https://github.com/lxbwolf

[14]

polaris: https://github.com/polaris1119

[15]

GCTT: https://github.com/studygolang/GCTT

[16]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ND_W1g0k0tKZNpC2ohDR0w