Json資料編碼和解碼

2020-07-16 10:05:18
資料結構要在網路中傳輸或儲存到檔案,就必須對其編碼和解碼;目前存在很多編碼格式:JSON,XML,gob,Google 緩衝協定等等。Go語言支援所有這些編碼格式。

結構可能包含二進位制資料,如果將其作為文字列印,那麼可讀性是很差的。另外結構內部可能包含匿名欄位,而不清楚資料的用意。

通過把資料轉換成純文字,使用命名的欄位來標註,讓其具有可讀性。這樣的資料格式可以通過網路傳輸,而且是與平台無關的,任何型別的應用都能夠讀取和輸出,不與作業系統和程式語言的型別相關。

下面是一些術語說明:
  • 資料結構 --> 指定格式 = 序列化 或 編碼(傳輸之前)
  • 指定格式 --> 資料格式 = 反序列化 或 解碼(傳輸之後)

序列化是在記憶體中把資料轉換成指定格式(data -> string),反之亦然(string -> data structure)編碼也是一樣的,只是輸出一個資料流(實現了 io.Writer 口);解碼是從一個資料流(實現了 io.Reader)輸出到一個資料結構。

也許大家比較熟悉 XML 格式,但有些時候 JSON 格式被作為首選,主要是由於其格式上非常簡潔。通常 JSON 被用於 web 後端和瀏覽器之間的通訊,但是在其它場景也同樣的有用。

這是一個簡短的 JSON 片段:
{
    "Person": {
        "FirstName": "Laura",
        "LastName": "Lynn"
    }
}
儘管 XML 被廣泛的應用,但是 JSON 更加簡潔、輕量(占用更少的記憶體、磁碟及網路頻寬)和更好的可讀性,這也說明它越來越受歡迎。

Go語言的 json 包可以讓你在程式中方便的讀取和寫入 JSON 資料。程式碼如下所示:
// json.go
package main
import (
    "encoding/json"
    "fmt"
    "log"
    "os"
)
type Address struct {
    Type string
    City string
    Country string
}
type VCard struct {
    FirstName string
    LastName string
    Addresses []*Address
    Remark string
}
func main() {
    pa := &Address{"private", "Aartselaar", "Belgium"}
    wa := &Address{"work", "Boom", "Belgium"}
    vc := VCard{"Jan", "Kersschot", []*Address{pa, wa}, "none"}
    // fmt.Printf("%v: n", vc) // {Jan Kersschot [0x126d2b80 0x126d2be0] none}:
    // JSON format:
    js, _ := json.Marshal(vc)
    fmt.Printf("JSON format: %s", js)
    // using an encoder:
    file, _ := os.OpenFile("vcard.json", os.O_CREATE|os.O_WRONLY, 0)
    defer file.Close()
    enc := json.NewEncoder(file)
    err := enc.Encode(vc)
    if err != nil {
        log.Println("Error in encoding json")
    }
}
json.Marshal() 的函數簽名是func Marshal(v interface{}) ([]byte, error),下面是資料編碼後的 JSON 文字(實際上是一個 []bytes):
{
    "FirstName": "Jan",
    "LastName": "Kersschot",
    "Addresses": [{
        "Type": "private",
        "City": "Aartselaar",
        "Country": "Belgium"
    }, {
        "Type": "work",
        "City": "Boom",
        "Country": "Belgium"
    }],
    "Remark": "none"
}
出於安全考慮,在 web 應用中最好使用json.MarshalforHTML()函數,其對資料執行 HTML 轉碼,所以文字可以被安全地嵌在 HTML <script> 標籤中。

JSON 與 Go 型別對應如下:
  • bool 對應 JSON 的 booleans
  • float64 對應 JSON 的 numbers
  • string 對應 JSON 的 strings
  • nil 對應 JSON 的 null

不是所有的資料都可以編碼為 JSON 型別:只有驗證通過的資料結構才能被編碼:
  • JSON 物件只支援字串型別的 key;要編碼一個 Go map 型別,map 必須是 map[string]T(T是 json 包中支援的任何型別)
  • Channel,複雜型別和函數型別不能被編碼
  • 不支援回圈資料結構;它將引起序列化進入一個無限迴圈
  • 指標可以被編碼,實際上是對指標指向的值進行編碼(或者指標是 nil)

反序列化:

UnMarshal() 的函數簽名是func Unmarshal(data []byte, v interface{}) error把 JSON 解碼為資料結構。

我們首先建立一個結構 Message 用來儲存解碼的資料:var m Message 並呼叫 Unmarshal(),解析 []byte 中的 JSON 資料並將結果存入指標 m 指向的值。

雖然反射能夠讓 JSON 欄位去嘗試匹配目標結構欄位;但是只有真正匹配上的欄位才會填充資料。欄位沒有匹配不會報錯,而是直接忽略掉。

解碼任意的資料:

json 包使用 map[string]interface{} 和 []interface{} 儲存任意的 JSON 物件和陣列;其可以被反序列化為任何的 JSON blob 儲存到介面值中。

來看這個 JSON 資料,被儲存在變數 b 中:

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

不用理解這個資料的結構,我們可以直接使用 Unmarshal 把這個資料編碼並儲存在介面值中:

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

f 指向的值是一個 map,key 是一個字串,value 是自身儲存作為空介面型別的值:
map[string]interface{} {
    "Name": "Wednesday",
    "Age": 6,
    "Parents": []interface{} {
        "Gomez",
        "Morticia",
    },
}
要存取這個資料,我們可以使用型別斷言。

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

我們可以通過 for range 語法和 type switch 來存取其實際型別:
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 []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 資料,同時可以確保型別安全。

解碼資料到結構:

如果我們事先知道 JSON 資料,可以定義一個適當的結構並對 JSON 資料反序列化。下面的例子中,我們將定義:
type FamilyMember struct {
    Name string
    Age int
    Parents []string
}
並對其反序列化:
var m FamilyMember
err := json.Unmarshal(b, &m)
程式實際上是分配了一個新的切片。這是一個典型的反序列化參照型別(指標、切片和 map)的例子。

編碼和解碼流

json 包提供 Decoder 和 Encoder 型別來支援常用 JSON 資料流讀寫。NewDecoder 和 NewEncoder 函數分別封裝了 io.Reader 和 io.Writer 介面。

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

要想把 JSON 直接寫入檔案,可以使用 json.NewEncoder 初始化檔案(或者任何實現 io.Writer 的型別),並呼叫 Encode();反過來與其對應的是使用 json.Decoder 和 Decode() 函數:

func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v interface{}) error

來看下介面是如何對實現進行抽象的:資料結構可以是任何型別,只要其實現了某種介面,目標或源資料要能夠被編碼就必須實現 io.Writer 或 io.Reader 介面。由於 Go語言中到處都實現了 Reader 和 Writer,因此 Encoder 和 Decoder 可被應用的場景非常廣泛,例如讀取或寫入 HTTP 連線、websockets 或檔案。