# XML 處理

XML 作為一種資料交換和資訊傳遞的格式已經十分普及。而隨著 Web 服務日益廣泛的應用，現在 XML 在日常的開發工作中也扮演了愈發重要的角色。這一小節， 我們將就 Go 語言標準套件中的 XML 相關處理的套件進行介紹。

這個小節不會涉及 XML 規範相關的內容（如需了解相關知識請參考其他文獻），而是介紹如何用 Go 語言來編解碼 XML 檔案相關的知識。

假如你是一名運維人員，你為你所管理的所有伺服器生成了如下內容的 xml 的配置檔案：

```markup
<?xml version="1.0" encoding="utf-8"?>
<servers version="1">
    <server>
        <serverName>Shanghai_VPN</serverName>
        <serverIP>127.0.0.1</serverIP>
    </server>
    <server>
        <serverName>Beijing_VPN</serverName>
        <serverIP>127.0.0.2</serverIP>
    </server>
</servers>
```

上面的 XML 文件描述了兩個伺服器的資訊，包含了伺服器名和伺服器的 IP 資訊，接下來的 Go 例子以此 XML 描述的資訊進行操作。

## 解析 XML

如何解析如上這個 XML 檔案呢？ 我們可以透過 xml 套件的 `Unmarshal` 函式來達到我們的目的

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

data 接收的是 XML 資料流，v 是需要輸出的結構，定義為 interface，也就是可以把 XML 轉換為任意的格式。我們這裡主要介紹 struct 的轉換，因為 struct 和 XML 都有類似樹結構的特徵。

範例程式碼如下：

```go
package main

import (
    "encoding/xml"
    "fmt"
    "io/ioutil"
    "os"
)

type Recurlyservers struct {
    XMLName     xml.Name `xml:"servers"`
    Version     string   `xml:"version,attr"`
    Svs         []server `xml:"server"`
    Description string   `xml:",innerxml"`
}

type server struct {
    XMLName    xml.Name `xml:"server"`
    ServerName string   `xml:"serverName"`
    ServerIP   string   `xml:"serverIP"`
}

func main() {
    file, err := os.Open("servers.xml") // For read access.
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }
    defer file.Close()
    data, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }
    v := Recurlyservers{}
    err = xml.Unmarshal(data, &v)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }

    fmt.Println(v)
}
```

XML 本質上是一種樹狀結構，而我們可以定義與之匹配的 go 語言的 struct 型別，然後透過 xml.Unmarshal 來將 xml 中的資料解析成對應的 struct 物件。如上例子輸出如下資料

```markup
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
<server>
    <serverName>Shanghai_VPN</serverName>
    <serverIP>127.0.0.1</serverIP>
</server>
<server>
    <serverName>Beijing_VPN</serverName>
    <serverIP>127.0.0.2</serverIP>
</server>
}
```

上面的例子中，將 xml 檔案解析成對應的 struct 物件是透過`xml.Unmarshal`來完成的，這個過程是如何實現的？可以看到我們的 struct 定義後面多了一些類似於`xml:"serverName"`這樣的內容，這個是 struct 的一個特性，它們被稱為 struct tag，它們是用來輔助反射的。我們來看一下 `Unmarshal` 的定義：

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

我們看到函式定義了兩個參數，第一個是 XML 資料流，第二個是儲存的對應型別，目前支援 struct、slice 和 string，XML 套件內部採用了反射來進行資料的對映，所以 v 裡面的欄位必須是匯出的。`Unmarshal`解析的時候 XML 元素和欄位怎麼對應起來的呢？這是有一個優先順序讀取流程的，首先會讀取 struct tag，如果沒有，那麼就會對應欄位名。必須注意一點的是解析的時候 tag、欄位名、XML 元素都是區分大小寫的的，所以必須一一對應欄位。

Go 語言的反射機制，可以利用這些 tag 資訊來將來自 XML 檔案中的資料反射成對應的 struct 物件，關於反射如何利用 struct tag 的更多內容請參閱 reflect 中的相關內容。

解析 XML 到 struct 的時候遵循如下的規則：

* 如果 struct 的一個欄位是 string 或者 \[]byte 型別且它的 tag 含有`",innerxml"`，Unmarshal 將會將此欄位所對應的元素內所有內嵌的原始 xml 累加到此欄位上，如上面例子 Description 定義。最後的輸出是

```markup
    <server>
        <serverName>Shanghai_VPN</serverName>
        <serverIP>127.0.0.1</serverIP>
    </server>
    <server>
        <serverName>Beijing_VPN</serverName>
        <serverIP>127.0.0.2</serverIP>
    </server>
```

* 如果 struct 中有一個叫做 XMLName，且型別為 xml.Name 欄位，那麼在解析的時候就會儲存這個 element 的名字到該欄位，如上面例子中的 servers。
* 如果某個 struct 欄位的 tag 定義中含有 XML 結構中 element 的名稱，那麼解析的時候就會把相應的 element 值賦值給該欄位，如上 servername 和 serverip 定義。
* 如果某個 struct 欄位的 tag 定義了中含有`",attr"`，那麼解析的時候就會將該結構所對應的 element 的與欄位同名的屬性的值賦值給該欄位，如上 version 定義。
* 如果某個 struct 欄位的 tag 定義 型如`"a>b>c"`，則解析的時候，會將 xml 結構 a 下面的 b 下面的 c 元素的值賦值給該欄位。
* 如果某個 struct 欄位的 tag 定義了`"-"`，那麼不會為該欄位解析匹配任何 xml 資料。
* 如果 struct 欄位後面的 tag 定義了`",any"`，如果他的子元素在不滿足其他的規則的時候就會匹配到這個欄位。
* 如果某個 XML 元素包含一條或者多條註釋，那麼這些註釋將被累加到第一個 tag 含有",comments"的欄位上，這個欄位的型別可能是 \[]byte 或 string，如果沒有這樣的欄位存在，那麼註釋將會被拋棄。

上面詳細講述了如何定義 struct 的 tag。 只要設定對了 tag，那麼 XML 解析就如上面範例般簡單，tag 和 XML 的 element 是一一對應的關係，如上所示，我們還可以透過 slice 來表示多個同級元素。

> 注意： 為了正確解析，go 語言的 xml 套件要求 struct 定義中的所有欄位必須是可匯出的（即首字母大寫）

## 輸出 XML

假若我們不是要解析如上所示的 XML 檔案，而是產生它，那麼在 go 語言中又該如何實現呢？ xml 套件中提供了 `Marshal` 和`MarshalIndent`兩個函式，來滿足我們的需求。這兩個函式主要的區別是第二個函式會增加字首和縮排，函式的定義如下所示：

```go
func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
```

兩個函式第一個參數是用來產生 XML 的結構定義型別資料，都是回傳產生的 XML 資料流。

下面我們來看一下如何輸出如上的 XML：

```go
package main

import (
    "encoding/xml"
    "fmt"
    "os"
)

type Servers struct {
    XMLName xml.Name `xml:"servers"`
    Version string   `xml:"version,attr"`
    Svs     []server `xml:"server"`
}

type server struct {
    ServerName string `xml:"serverName"`
    ServerIP   string `xml:"serverIP"`
}

func main() {
    v := &Servers{Version: "1"}
    v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
    v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
    output, err := xml.MarshalIndent(v, "  ", "    ")
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
    os.Stdout.Write([]byte(xml.Header))

    os.Stdout.Write(output)
}
```

上面的程式碼輸出如下資訊：

```markup
<?xml version="1.0" encoding="UTF-8"?>
<servers version="1">
<server>
    <serverName>Shanghai_VPN</serverName>
    <serverIP>127.0.0.1</serverIP>
</server>
<server>
    <serverName>Beijing_VPN</serverName>
    <serverIP>127.0.0.2</serverIP>
</server>
</servers>
```

和我們之前定義的檔案的格式一模一樣，之所以會有`os.Stdout.Write([]byte(xml.Header))` 這句程式碼的出現，是因為`xml.MarshalIndent`或者`xml.Marshal`輸出的資訊都是不帶 XML 頭的，為了產生正確的 xml 檔案，我們使用了 xml 套件預定義的 Header 變數。

我們看到 `Marshal` 函式接收的參數 v 是 interface{}型別的，即它可以接受任意型別的參數，那麼 xml 套件，根據什麼規則來產生相應的 XML 檔案呢？

* 如果 v 是 array 或者 slice，那麼輸出每一個元素，類似value
* 如果 v 是指標，那麼會 Marshal 指標指向的內容，如果指標為空，什麼都不輸出
* 如果 v 是 interface，那麼就處理 interface 所包含的資料
* 如果 v 是其他資料型別，就會輸出這個資料型別所擁有的欄位資訊

產生的 XML 檔案中的 element 的名字又是根據什麼決定的呢？元素名按照如下優先順序從 struct 中取得：

* 如果 v 是 struct，XMLName 的 tag 中定義的名稱
* 型別為 xml.Name 的名叫 XMLName 的欄位的值
* 透過 struct 中欄位的 tag 來取得
* 透過 struct 的欄位名用來取得
* marshall 的型別名稱

我們應如何設定 struct 中欄位的 tag 資訊以控制最終 xml 檔案的產生呢？

* XMLName 不會被輸出
* tag 中含有`"-"`的欄位不會輸出
* tag 中含有`"name,attr"`，會以 name 作為屬性名，欄位值作為值輸出為這個 XML 元素的屬性，如上 version 欄位所描述
* tag 中含有`",attr"`，會以這個 struct 的欄位名作為屬性名輸出為 XML 元素的屬性，類似上一條，只是這個 name 預設是欄位名了。
* tag 中含有`",chardata"`，輸出為 xml 的 character data 而非 element。
* tag 中含有`",innerxml"`，將會被原樣輸出，而不會進行常規的編碼過程
* tag 中含有`",comment"`，將被當作 xml 註釋來輸出，而不會進行常規的編碼過程，欄位值中不能含有"--"字串
* tag 中含有`"omitempty"`，如果該欄位的值為空值那麼該欄位就不會被輸出到 XML，空值包括：false、0、nil 指標或 nil 介面，任何長度為 0 的 array, slice, map 或者 string
* tag 中含有`"a>b>c"`，那麼就會迴圈輸出三個元素 a 包含 b，b 包含 c，例如如下程式碼就會輸出

```markup
    FirstName string   `xml:"name>first"`
    LastName  string   `xml:"name>last"`

    <name>
    <first>Asta</first>
    <last>Xie</last>
    </name>
```

上面我們介紹了如何使用 Go 語言的 xml 套件來編/解碼 XML 檔案，重要的一點是對 XML 的所有操作都是透過 struct tag 來實現的，所以學會對 struct tag 的運用變得非常重要，在文章中我們簡要的列舉了如何定義 tag。更多內容或 tag 定義請參看相應的官方資料。

## links

* [目錄](https://github.com/doggy8088/build-web-application-with-golang-zhtw/tree/4cbbaa31bdfc3678915eb91f23db2f1bad554a20/preface.md)
* 上一節: [文字處理](https://willh.gitbook.io/build-web-application-with-golang-zhtw/07.0)
* 下一節: [Json 處理](https://willh.gitbook.io/build-web-application-with-golang-zhtw/07.0/07.2)
