Go 部落格

JSON 和 Go

Andrew Gerrand
2011 年 1 月 25 日

引言

JSON(JavaScript 物件表示法)是一種簡單的資料交換格式。在語法上,它類似於 JavaScript 的物件和列表。它最常用於 Web 後端與在瀏覽器中執行的 JavaScript 程式之間的通訊,但它也用於許多其他地方。它的主頁 json.org 提供了對該標準的清晰簡潔的定義。

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

編碼

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

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

給定 Go 資料結構 Message

type Message struct {
    Name string
    Body string
    Time int64
}

以及 Message 的一個例項

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

我們可以使用 json.Marshal 將 m 編組為 JSON 編碼的版本。

b, err := json.Marshal(m)

如果一切正常,err 將為 nil,而 b 將是一個 []byte,其中包含此 JSON 資料。

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

只有可以表示為有效 JSON 的資料結構才會被編碼。

  • JSON 物件僅支援字串作為鍵;要編碼 Go 對映型別,它必須是 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 將僅解碼它能在目標型別中找到的欄位。在這種情況下,只有 m 的 Name 欄位會被填充,Food 欄位將被忽略。當您希望從大型 JSON 資料塊中只提取幾個特定欄位時,此行為特別有用。這也意味著目標結構體中的任何未匯出欄位都不會受到 Unmarshal 的影響。

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

泛用 JSON 與 interface

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 型別是:

  • bool 用於 JSON 布林值,

  • float64 用於 JSON 數字,

  • string 用於 JSON 字串,以及

  • nil 用於 JSON null。

解碼任意資料

考慮此 JSON 資料,它儲存在變數 b 中。

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 語句遍歷對映,並使用型別開關將值作為其具體型別進行訪問。

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 資料中存在的資料結構。為了知道要處理哪些訊息,程式設計師只需測試 CmdMsg 是否不為 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)
        }
    }
}

由於 Reader 和 Writer 的普遍性,這些 EncoderDecoder 型別可以在廣泛的場景中使用,例如讀寫 HTTP 連線、WebSockets 或檔案。

參考

有關更多資訊,請參閱 json 包文件。有關 json 的示例用法,請參閱 jsonrpc 包 的原始碼檔案。

下一篇文章: Go 變得更加穩定
上一篇文章: Go 切片:用法和內部
部落格索引