Go 部落格

JSON 和 Go

Andrew Gerrand
2011 年 1 月 25 日

簡介

JSON(JavaScript 物件表示法)是一種簡易資料交換格式。其語法類似 JavaScript 的物件和清單。最常使用於網路後端與瀏覽器中執行的 JavaScript 程式之間的通訊,但也在許多其他地方使用。其首頁 json.org 提供了令人驚豔、清楚且簡潔的標準定義。

使用 json 套件,便可以從 Go 程式中輕鬆讀取和寫入 JSON 資料。

編碼

我們使用 Marshal 函式編碼 JSON 資料。

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

給定 Go 資料結構 訊息

type Message struct {
    Name string
    Body string
    Time int64
}

以及 訊息 的一個執行個體

m := Message{"Alice", "Hello", 1294706395881547000}

我們可以用 json.Marshal 彙整 m 的 JSON 編碼版本

b, err := json.Marshal(m)

一切都順利的話,err 會是 nil,而 b 會是包含此 JSON 資料的 []byte

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

僅會編碼可表示為有效的 JSON 的資料結構

  • JSON 物件僅支援將字串作為鍵;要編碼 Go map 型別,它必須為 map[string]T 形式(其中 T 為 json 套件支援的任何 Go 型別)。

  • 通道、複雜數和函式型別無法編碼。

  • 不支援循環資料結構;它們將導致 Marshal 進入無限迴圈。

  • 指標將編碼為它們所指向的值(如果指標為 nil 則編碼為 ’null’)。

json 套件僅存取結構型別的公開欄位(以大寫字母開頭的欄位)。因此,JSON 輸出中只會出現結構的公開欄位。

解碼

要解碼 JSON 資料,我們會使用 Unmarshal 函式。

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

我們必須先建立一個儲存解碼資料的地方

var m Message

然後呼叫 json.Unmarshal,傳遞 JSON 資料的 []byte 和指向 m 的指標

err := json.Unmarshal(b, &m)

如果 b 包含符合 m 格式的有效 JSON,則呼叫後 err 將為 nil,且 b 中的資料將儲存在結構 m 中,就好像透過賦值一樣:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Unmarshal 如何辨識用於儲存解碼資料的欄位?對於特定的 JSON 鍵 "Foo"Unmarshal 會從目的結構的欄位中尋找(依優先順序為):

  • 具有標籤 "Foo" 的公開欄位(有關結構標籤的詳細資訊,請參閱 Go 規範),

  • 名為 "Foo" 的公開欄位,或

  • 名為 "FOO""FoO" 或一些與 "Foo" 不分大小寫的配對的公開欄位。

當 JSON 資料的結構與 Go 型別不完全相符時,會發生什麼情況?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal 僅會解碼它能在目的型別中找到的欄位。在本例中,只會填充 mName 欄位,而 Food 欄位將被忽略。此行為特別適用於您希望從大量 JSON 資料區塊中選取特定幾個欄位時。它也表示目的結構中任何未公開的欄位都不會受到 Unmarshal 影響。

但是,如果您事先不知道 JSON 資料的結構怎麼辦?

具有介面的通用 JSON

interface{}(空介面)型別描述一個不含任何方法的介面。每個 Go 型別至少實作零個方法,因此滿足空介面。

空介面作為一個通用容器型別

var i interface{}
i = "a string"
i = 2011
i = 2.777

型別斷言存取基礎的具體型別

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

或者,如果基礎型別不明,則型別轉換決定型別

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i isn't one of the types above
}

json 套件使用 `map[string]interface{}` 和 `[]interface{}` 值來儲存任意的 JSON 物件和陣列;它會安心地解析任何有效的 JSON 資訊塊到一個一般的 `interface{}` 值。 預設的具體 Go 類型是

  • JSON 布林值的 `bool`、

  • JSON 數字的 `float64`、

  • JSON 字串的 `string`,以及

  • JSON null 的 `nil`。

解碼任意資料

考慮儲存在變數 `b` 的這個 JSON 資料

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

不知道這些資料的結構,我們可以用 `Unmarshal` 將它們解碼到一個 `interface{}` 值

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

此時在 `f` 的 Go 值會是其鍵值為字串且其值本身儲存為空介面值的映射

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

若要存取這些資料,我們可以使用類型斷言來存取 `f` 底層的 `map[string]interface{}`

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

接著我們可以用 `range` 陳述式遍歷映射,並使用 `type switch` 來存取其值作為其具體類型

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", 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")
    }
}

這樣,您就能處理不知道的 JSON 資料,同時仍然享受類型安全的優點。

參考類型

我們來定義一個 Go 類型來包含先前範例的資料

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

將那些資料轉碼成 `FamilyMember` 值運作如預期,但若仔細看,我們會看到發生了一件很厲害的事。透過 `var` 陳述式,我們配置了一個 `FamilyMember` 結構,然後提供該值的指標給 `Unmarshal`,但在那個時候,`Parents` 欄位是一個 `nil` 片段值。若要填入 `Parents` 欄位,`Unmarshal` 在幕後配置了一個新的片段。這是 `Unmarshal` 與支援的參考類型(指標、片段和映射)運作的典型方式。

考慮轉碼成這個資料結構

type Foo struct {
    Bar *Bar
}

若 JSON 物件有一個 `Bar` 欄位,`Unmarshal` 會配置一個新的 `Bar` 並填入。若沒有,`Bar` 會被留下來成為一個 `nil` 指標。

從這裡產生一個有用的模式:若您有一個會收到幾個不同訊息類型的應用程式,您可以定義像這樣的「接收器」結構

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

而且發送者可以填入頂層 JSON 物件的 `Cmd` 欄位和/或 `Msg` 欄位,視他們要傳達的訊息種類而定。`Unmarshal` 在將 JSON 解碼成 `IncomingMessage` 結構時,只會配置出現在 JSON 資料中的資料結構。若要了解要處理哪些訊息,程式設計師只需要測試 `Cmd` 或 `Msg` 其中一個是不是 `nil` 即可。

串流編碼器和解碼器

json 套件提供 DecoderEncoder 類型,以支援讀寫 JSON 資料串流的常見作業。 NewDecoderNewEncoder 函式打包 io.Readerio.Writer 介面類型。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

這裡有一個範例程式,從標準輸入中讀取一系列 JSON 物件,從每個物件中去除所有欄位,但保留 Name 欄位,然後將這些物件寫入標準輸出

package main

import (
    "encoding/json"
    "log"
    "os"
)

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

由於 Readers 和 Writers 十分普遍,所以這些 EncoderDecoder 類型可廣泛用於下列情況,例如讀寫 HTTP 連線、WebSocket 或檔案。

參考文件

請參閱 json 套件文件,以取得更多資訊。請參閱 jsonrpc 套件 的原始檔,以取得使用 json 的範例。

下一篇文章: Go 越來越穩定
上一篇文章: Go 片段:使用方式和內部結構
部落格索引