Go 部落格
JSON 和 Go
引言
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 資料中存在的資料結構。為了知道要處理哪些訊息,程式設計師只需測試 Cmd 或 Msg 是否不為 nil。
流式編碼器和解碼器
json 包提供了 Decoder 和 Encoder 型別來支援讀取和寫入 JSON 資料流的常見操作。NewDecoder 和 NewEncoder 函式包裝了 io.Reader 和 io.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 的普遍性,這些 Encoder 和 Decoder 型別可以在廣泛的場景中使用,例如讀寫 HTTP 連線、WebSockets 或檔案。
參考
下一篇文章: Go 變得更加穩定
上一篇文章: Go 切片:用法和內部
部落格索引