Go語言解碼未知結構的JSON資料

2020-07-16 10:05:20
Go語言內建的 encoding/json 標準庫提供了對 JSON 資料進行編解碼的功能。在實際開發過程中,有時候我們可能並不知道要解碼的 JSON 資料結構是什麼樣子的,這個時候應該怎麼處理呢?

如果要解碼一段未知結構的 JSON,只需將這段 JSON 資料解碼輸出到一個空介面即可。關於 JSON 資料的編碼和解碼的詳細介紹可以閱讀《Json資料編碼和解碼》一節。

型別轉換規則

在前面介紹介面的時候,我們提到基於Go語言的物件導向特性,可以通過空介面來表示任何型別,這同樣也適用於對未知結構的 JSON 資料進行解碼,只需要將這段 JSON 資料解碼輸出到一個空介面即可。

在實際解碼過程中,JSON 結構裡邊的資料元素將做如下型別轉換:
  • 布林值將會轉換為Go語言的 bool 型別;
  • 數值會被轉換為Go語言的 float64 型別;
  • 字串轉換後還是 string 型別;
  • JSON 陣列會轉換為 []interface{} 型別;
  • JSON 物件會轉換為 map[string]interface{} 型別;
  • null 值會轉換為 nil。

在Go語言標準庫 encoding/json 中,可以使用map[string]interface{}[]interface{}型別的值來分別存放未知結構的 JSON 物件或陣列。

【範例 1】解析 JSON 資料,並將結果對映到空介面物件:
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    u3 := []byte(`{"name": "C語言中文網", "website": "http://c.biancheng.net/", "course": ["Golang", "PHP", "JAVA", "C"]}`)
    var user4 interface{}
    err := json.Unmarshal(u3, &user4)
    if err != nil {
        fmt.Printf("JSON 解碼失敗:%vn", err)
        return
    }
    fmt.Printf("JSON 解碼結果: %#vn", user4)
}
上述程式碼中,user4 被定義為一個空介面;json.Unmarshal() 函數將一個 JSON 物件 u3 解碼到空介面 user4 中,最終 user4 將會是一個鍵值對的map[string]interface{}結構。

執行結果如下:

JSON 解碼結果: map[string]interface {}{"course":[]interface {}{"Golang", "PHP", "JAVA", "C"}, "name":"C語言中文網", "website":"http://c.biancheng.net/"}

因為 u3 整體上是一個 JSON 物件,內部屬性也會遵循上述型別的轉化規則進行轉換。

存取解碼後資料

要存取解碼後的資料結構,需要先判斷目標結構是否為預期的資料型別,然後我們可以通過 for 迴圈搭配 range 語句存取解碼後的目標資料:
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    u3 := []byte(`{"name": "C語言中文網", "website": "http://c.biancheng.net/", "course": ["Golang", "PHP", "JAVA", "C"]}`)
    var user4 interface{}
    err := json.Unmarshal(u3, &user4)
    if err != nil {
        fmt.Printf("JSON 解碼失敗:%vn", err)
        return
    }
    user5, ok := user4.(map[string]interface{})
    if ok {
        for k, v := range user5 {
            switch v2 := v.(type) {
            case string:
                fmt.Println(k, "is string", v2)
            case int:
                fmt.Println(k, "is int", v2)
            case bool:
                fmt.Println(k, "is bool", v2)
            case []interface{}:
                fmt.Println(k, "is an array:")
                for i, iv := range v2 {
                    fmt.Println(i, iv)
                }
            default:
                fmt.Println(k, "型別未知")
            }
        }
    }
}
執行結果如下:

name is string C語言中文網
website is string http://c.biancheng.net/
course is an array:
0 Golang
1 PHP
2 JAVA
3 C

雖然有些煩瑣,但的確是一種解碼未知結構的 JSON 資料的安全方式。

JSON 的流式讀寫

Go語言內建的 encoding/json 包還提供了 Decoder 和 Encoder 兩個型別,用於支援 JSON 資料的流式讀寫,並提供了 NewDecoder() 和 NewEncoder() 兩個函數用於具體實現:

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

【範例 2】從標準輸入流中讀取 JSON 資料,然後將其解碼,最後再寫入到標準輸出流中:
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
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}
執行上面的程式碼,我們需要先輸入 JSON 結構資料供標準輸入流 os.Stdin 讀取,讀取到資料後,會通過 json.NewDecoder 返回的解碼器對其進行解碼,最後再通過 json.NewEncoder 返回的編碼器將資料編碼後寫入標準輸出流 os.Stdout 並列印出來:

go run main.go
{"name": "C語言中文網", "website": "http://c.biancheng.net/", "course": ["Golang", "PHP", "JAVA", "C"]}
{"course":["Golang","PHP","JAVA","C"],"name":"C語言中文網","website":"http://c.biancheng.net/"}

其中,第二行為我們輸入的內容,第三行為輸出內容。

使用 Decoder 和 Encoder 對資料流進行處理可以應用得更為廣泛些,比如讀寫 HTTP 連線、WebSocket 或檔案等,Go語言標準庫中的 net/rpc/jsonrpc 就是一個應用了 Decoder 和 Encoder 的實際例子:
// NewServerCodec returns a new rpc.ServerCodec using JSON-RPC on conn.
func NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec {
    return &serverCodec{
        dec:     json.NewDecoder(conn),
        enc:     json.NewEncoder(conn),
        c:       conn,
        pending: make(map[uint64]*json.RawMessage),
    }
}