Files
build-web-application-with-…/zh-tw/07.2.md
2019-06-22 23:41:28 +08:00

244 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 來表示,結果描述如下:
```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 套件中有如下函式
```Go
func Unmarshal(data []byte, v interface{}) error
```
透過這個函式我們就可以實現解析的目的,詳細的解析例子請看如下程式碼:
```Go
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 資料
```Go
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
```
如果在我們不知道他的結構的情況下,我們把他解析到 interface{} 裡面
```Go
var f interface{}
err := json.Unmarshal(b, &f)
```
這個時候 f 裡面儲存了一個 map 型別,他們的 key 是 string值儲存在空的 interface{} 裡
```Go
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
```
那麼如何來訪問這些資料呢?透過斷言的方式:
```Go
m := f.(map[string]interface{})
```
透過斷言之後,你就可以透過如下方式來訪問裡面的資料了
```Go
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 時相當方便,詳細例子如下所示:
```Go
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` 函式來處理,函式定義如下:
```Go
func Marshal(v interface{}) ([]byte, error)
```
假設我們還是需要產生上面的伺服器列表資訊,那麼如何來處理呢?請看下面的例子:
```Go
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))
}
```
輸出如下內容:
```json
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
```
我們看到上面的輸出欄位名的首字母都是大寫的如果你想用小寫的首字母怎麼辦呢把結構體的欄位名改成首字母小寫的JSON 輸出的時候必須注意,只有匯出的欄位才會被輸出,如果修改欄位名,那麼就會發現什麼都不會輸出,所以必須透過 struct tag 定義來實現:
```Go
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 字串
舉例來說:
```Go
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)
```
會輸出以下內容:
```json
{"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 開發相當重要。
## links
* [目錄](<preface.md>)
* 上一節:[XML 處理](<07.1.md>)
* 下一節:[正則處理](<07.3.md>)