Files
build-web-application-with-…/zh-tw/07.2.md
2019-02-26 01:40:54 +08:00

8.8 KiB
Raw Blame History

7.2 JSON處理

JSONJavascript Object Notation是一種輕量級的資料交換語言以文字為基礎具有自我描述性且易於讓人閱讀。儘管JSON是Javascript的一個子集但JSON是獨立於語言的文字格式並且採用了類似於C語言家族的一些習慣。JSON與XML最大的不同在於XML是一個完整的標記語言而JSON不是。JSON由於比XML更小、更快更易解析,以及瀏覽器的內建快速解析支援,使得其更適用於網路資料傳輸領域。目前我們看到很多的開放平臺基本上都是採用了JSON作為他們的資料互動的介面。既然JSON在Web開發中如此重要那麼Go語言對JSON支援的怎麼樣呢Go語言的標準函式庫已經非常好的支援了JSON可以很容易的對JSON資料進行編、解碼的工作。

前一小節的運維的例子用json來表示結果描述如下


{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}

本小節餘下的內容將以此JSON資料為基礎來介紹go語言的json套件對JSON資料的編、解碼。

解析JSON

解析到結構體

假如有了上面的JSON串那麼我們如何來解析這個JSON串呢Go的JSON套件中有如下函式


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

透過這個函式我們就可以實現解析的目的,詳細的解析例子請看如下程式碼:


package main

import (
	"encoding/json"
	"fmt"
)

type Server struct {
	ServerName string
	ServerIP   string
}

type Serverslice struct {
	Servers []Server
}

func main() {
	var s Serverslice
	str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
	json.Unmarshal([]byte(str), &s)
	fmt.Println(s)
}

在上面的示例程式碼中我們首先定義了與json資料對應的結構體陣列對應slice欄位名對應JSON裡面的KEY在解析的時候如何將json資料與struct欄位相匹配呢例如JSON的key是Foo,那麼怎麼找對應的欄位呢?

  • 首先查詢tag含有Foo的可匯出的struct欄位(首字母大寫)
  • 其次查詢欄位名是Foo的匯出欄位
  • 最後查詢類似FOO或者FoO這樣的除了首字母之外其他大小寫不敏感的匯出欄位

聰明的你一定注意到了這一點:能夠被賦值的欄位必須是可匯出欄位(即首字母大寫。同時JSON解析的時候只會解析能找得到的欄位找不到的欄位會被忽略這樣的一個好處是當你接收到一個很大的JSON資料結構而你卻只想取得其中的部分資料的時候你只需將你想要的資料對應的欄位名大寫即可輕鬆解決這個問題。

解析到interface

上面那種解析方式是在我們知曉被解析的JSON資料的結構的前提下采取的方案如果我們不知道被解析的資料的格式又應該如何來解析呢

我們知道interface{}可以用來儲存任意資料型別的物件這種資料結構正好用於儲存解析的未知結構的json資料的結果。JSON套件中採用map[string]interface{}和[]interface{}結構來儲存任意的JSON物件和陣列。Go型別和JSON型別的對應關係如下

  • bool 代表 JSON booleans,
  • float64 代表 JSON numbers,
  • string 代表 JSON strings,
  • nil 代表 JSON null.

現在我們假設有如下的JSON資料


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

如果在我們不知道他的結構的情況下我們把他解析到interface{}裡面


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

這個時候f裡面儲存了一個map型別他們的key是string值儲存在空的interface{}裡


f = map[string]interface{}{
	"Name": "Wednesday",
	"Age":  6,
	"Parents": []interface{}{
		"Gomez",
		"Morticia",
	},
}

那麼如何來訪問這些資料呢?透過斷言的方式:


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

透過斷言之後,你就可以透過如下方式來訪問裡面的資料了


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 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")
	}
}

透過上面的示例可以看到透過interface{}與type assert的配合我們就可以解析未知結構的JSON數了。

上面這個是官方提供的解決方案其實很多時候我們透過型別斷言操作起來不是很方便目前bitly公司開源了一個叫做simplejson的套件,在處理未知結構體的JSON時相當方便詳細例子如下所示


js, err := NewJson([]byte(`{
	"test": {
		"array": [1, "2", 3],
		"int": 10,
		"float": 5.150,
		"bignum": 9223372036854775807,
		"string": "simplejson",
		"bool": true
	}
}`))

arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()

可以看到使用這個函式庫操作JSON比起官方套件來說簡單的多,詳細的請參考如下地址:https://github.com/bitly/go-simplejson

產生JSON

我們開發很多應用的時候最後都是要輸出JSON資料串那麼如何來處理呢JSON套件裡面透過Marshal函式來處理,函式定義如下:


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

假設我們還是需要產生上面的伺服器列表資訊,那麼如何來處理呢?請看下面的例子:


package main

import (
	"encoding/json"
	"fmt"
)

type Server struct {
	ServerName string
	ServerIP   string
}

type Serverslice struct {
	Servers []Server
}

func main() {
	var s Serverslice
	s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
	s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
	b, err := json.Marshal(s)
	if err != nil {
		fmt.Println("json err:", err)
	}
	fmt.Println(string(b))
}

輸出如下內容:


{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}

我們看到上面的輸出欄位名的首字母都是大寫的如果你想用小寫的首字母怎麼辦呢把結構體的欄位名改成首字母小寫的JSON輸出的時候必須注意只有匯出的欄位才會被輸出如果修改欄位名那麼就會發現什麼都不會輸出所以必須透過struct tag定義來實現


type Server struct {
	ServerName string `json:"serverName"`
	ServerIP   string `json:"serverIP"`
}

type Serverslice struct {
	Servers []Server `json:"servers"`
}

透過修改上面的結構體定義輸出的JSON串就和我們最開始定義的JSON串保持一致了。

針對JSON的輸出我們在定義struct tag的時候需要注意的幾點是:

  • 欄位的tag是"-"那麼這個欄位不會輸出到JSON
  • tag中帶有自訂名稱那麼這個自訂名稱會出現在JSON的欄位名中例如上面例子中serverName
  • tag中如果帶有"omitempty"選項那麼如果該欄位值為空就不會輸出到JSON串中
  • 如果欄位型別是bool, string, int, int64等而tag中帶有",string"選項那麼這個欄位在輸出到JSON的時候會把該欄位對應的值轉換成JSON字串

舉例來說:


type Server struct {
	// ID 不會匯出到JSON中
	ID int `json:"-"`

	// ServerName2 的值會進行二次JSON編碼
	ServerName  string `json:"serverName"`
	ServerName2 string `json:"serverName2,string"`

	// 如果 ServerIP 為空則不輸出到JSON串中
	ServerIP   string `json:"serverIP,omitempty"`
}

s := Server {
	ID:         3,
	ServerName:  `Go "1.0" `,
	ServerName2: `Go "1.0" `,
	ServerIP:   ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)

會輸出以下內容:


{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}

Marshal函式只有在轉換成功的時候才會返回資料在轉換的過程中我們需要注意幾點

  • JSON物件只支援string作為key所以要編碼一個map那麼必須是map[string]T這種型別(T是Go語言中任意的型別)
  • Channel, complex和function是不能被編碼成JSON的
  • 巢狀的資料是不能編碼的不然會讓JSON編碼進入死迴圈
  • 指標在編碼的時候會輸出指標指向的內容而空指標會輸出null

本小節我們介紹瞭如何使用Go語言的json標準套件來編解碼JSON資料同時也簡要介紹瞭如何使用第三方套件go-simplejson來在一些情況下簡化操作學會並熟練運用它們將對我們接下來的Web開發相當重要。