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 切片:用法和內部
部落格索引