JSON 處理

JSON(Javascript Object Notation)是一種輕量級的資料交換語言,以文字為基礎,具有自我描述性且易於讓人閱讀。儘管 JSON 是 Javascript 的一個子集,但 JSON 是獨立於語言的文字格式,並且採用了類似於 C 語言家族的一些習慣。JSON 與 XML 最大的不同在於 XML 是一個完整的標記語言,而 JSON 不是。JSON 由於比 XML 更小、更快,更易解析,以及瀏覽器的內建快速解析支援,使得其更適用於網路資料傳輸領域。目前我們看到很多的開放平臺,基本上都是採用了 JSON 作為他們的資料互動的介面。既然 JSON 在 Web 開發中如此重要,那麼 Go 語言對 JSON 支援的怎麼樣呢?Go 語言的標準函式庫已經非常好的支援了 JSON,可以很容易的對 JSON 資料進行編、解碼的工作。

前一小節的運維的例子用 json 來表示,結果描述如下:

{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}

本小節餘下的內容將以此 JSON 資料為基礎,來介紹 go 語言的 json 套件對 JSON 資料的編、解碼。

解析 JSON

解析到結構體

假如有了上面的 JSON 串,那麼我們如何來解析這個 JSON 串呢?Go 的 JSON 套件中有如下函式

func Unmarshal(data []byte, v interface{}) error

透過這個函式我們就可以實現解析的目的,詳細的解析例子請看如下程式碼:

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    ServerName string
    ServerIP   string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
    json.Unmarshal([]byte(str), &s)
    fmt.Println(s)
}

在上面的範例程式碼中,我們首先定義了與 json 資料對應的結構體,陣列對應 slice,欄位名對應 JSON 裡面的 KEY,在解析的時候,如何將 json 資料與 struct 欄位相匹配呢?例如 JSON 的 key 是Foo,那麼怎麼找對應的欄位呢?

  • 首先查詢 tag 含有 Foo 的可匯出的 struct 欄位(首字母大寫)

  • 其次查詢欄位名是 Foo 的匯出欄位

  • 最後查詢類似 FOO 或者 FoO 這樣的除了首字母之外其他大小寫不敏感的匯出欄位

聰明的你一定注意到了這一點:能夠被賦值的欄位必須是可匯出欄位(即首字母大寫)。同時 JSON 解析的時候只會解析能找得到的欄位,找不到的欄位會被忽略,這樣的一個好處是:當你接收到一個很大的 JSON 資料結構而你卻只想取得其中的部分資料的時候,你只需將你想要的資料對應的欄位名大寫,即可輕鬆解決這個問題。

解析到 interface

上面那種解析方式是在我們知曉被解析的 JSON 資料的結構的前提下采取的方案,如果我們不知道被解析的資料的格式,又應該如何來解析呢?

我們知道 interface{} 可以用來儲存任意資料型別的物件,這種資料結構正好用於儲存解析的未知結構的 json 資料的結果。JSON 套件中採用 map[string]interface{} 和[]interface{}結構來儲存任意的 JSON 物件和陣列。Go 型別和 JSON 型別的對應關係如下:

  • bool 代表 JSON booleans,

  • float64 代表 JSON numbers,

  • string 代表 JSON strings,

  • nil 代表 JSON null.

現在我們假設有如下的 JSON 資料

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

如果在我們不知道他的結構的情況下,我們把他解析到 interface{} 裡面

var f interface{}
err := json.Unmarshal(b, &f)

這個時候 f 裡面儲存了一個 map 型別,他們的 key 是 string,值儲存在空的 interface{} 裡

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

那麼如何來存取這些資料呢?透過斷言的方式:

m := f.(map[string]interface{})

透過斷言之後,你就可以透過如下方式來存取裡面的資料了

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case int:
        fmt.Println(k, "is int", vv)
    case float64:
        fmt.Println(k,"is float64",vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

透過上面的範例可以看到,透過 interface{} 與 type assert 的配合,我們就可以解析未知結構的 JSON 數了。

上面這個是官方提供的解決方案,其實很多時候我們透過型別斷言,操作起來不是很方便,目前 bitly 公司開源了一個叫做 simplejson 的套件,在處理未知結構體的 JSON 時相當方便,詳細例子如下所示:

js, err := NewJson([]byte(`{
    "test": {
        "array": [1, "2", 3],
        "int": 10,
        "float": 5.150,
        "bignum": 9223372036854775807,
        "string": "simplejson",
        "bool": true
    }
}`))

arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()

可以看到,使用這個函式庫操作 JSON 比起官方套件來說,簡單的多,詳細的請參考如下地址:https://github.com/bitly/go-simplejson

產生 JSON

我們開發很多應用的時候,最後都是要輸出 JSON 資料串,那麼如何來處理呢?JSON 套件裡面透過 Marshal 函式來處理,函式定義如下:

func Marshal(v interface{}) ([]byte, error)

假設我們還是需要產生上面的伺服器列表資訊,那麼如何來處理呢?請看下面的例子:

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    ServerName string
    ServerIP   string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
    s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
    b, err := json.Marshal(s)
    if err != nil {
        fmt.Println("json err:", err)
    }
    fmt.Println(string(b))
}

輸出如下內容:

{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}

我們看到上面的輸出欄位名的首字母都是大寫的,如果你想用小寫的首字母怎麼辦呢?把結構體的欄位名改成首字母小寫的?JSON 輸出的時候必須注意,只有匯出的欄位才會被輸出,如果修改欄位名,那麼就會發現什麼都不會輸出,所以必須透過 struct tag 定義來實現:

type Server struct {
    ServerName string `json:"serverName"`
    ServerIP   string `json:"serverIP"`
}

type Serverslice struct {
    Servers []Server `json:"servers"`
}

透過修改上面的結構體定義,輸出的 JSON 串就和我們最開始定義的 JSON 串保持一致了。

針對 JSON 的輸出,我們在定義 struct tag 的時候需要注意的幾點是:

  • 欄位的 tag 是"-",那麼這個欄位不會輸出到 JSON

  • tag 中帶有自訂名稱,那麼這個自訂名稱會出現在 JSON 的欄位名中,例如上面例子中 serverName

  • tag 中如果帶有"omitempty"選項,那麼如果該欄位值為空,就不會輸出到 JSON 串中

  • 如果欄位型別是 bool, string, int, int64 等,而 tag 中帶有",string"選項,那麼這個欄位在輸出到 JSON 的時候會把該欄位對應的值轉換成 JSON 字串

舉例來說:

type Server struct {
    // ID 不會匯出到 JSON 中
    ID int `json:"-"`

    // ServerName2 的值會進行二次 JSON 編碼
    ServerName  string `json:"serverName"`
    ServerName2 string `json:"serverName2,string"`

    // 如果 ServerIP 為空,則不輸出到 JSON 串中
    ServerIP   string `json:"serverIP,omitempty"`
}

s := Server {
    ID:         3,
    ServerName:  `Go "1.0" `,
    ServerName2: `Go "1.0" `,
    ServerIP:   ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)

會輸出以下內容:

{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}

Marshal 函式只有在轉換成功的時候才會回傳資料,在轉換的過程中我們需要注意幾點:

  • JSON 物件只支援 string 作為 key,所以要編碼一個 map,那麼必須是 map[string]T 這種型別(T 是 Go 語言中任意的型別)

  • Channel, complex 和 function 是不能被編碼成 JSON 的

  • 巢狀的資料是不能編碼的,不然會讓 JSON 編碼進入無窮遞迴

  • 指標在編碼的時候會輸出指標指向的內容,而空指標會輸出 null

本小節,我們介紹了如何使用 Go 語言的 json 標準套件來編解碼 JSON 資料,同時也簡要介紹了如何使用第三方套件go-simplejson來在一些情況下簡化操作,學會並熟練運用它們將對我們接下來的 Web 開發相當重要。

Last updated