Go XML處理

2022-05-13 17:44 更新

XML作為一種數(shù)據(jù)交換和信息傳遞的格式已經(jīng)十分普及。而隨著Web服務(wù)日益廣泛的應(yīng)用,現(xiàn)在XML在日常的開(kāi)發(fā)工作中也扮演了愈發(fā)重要的角色。這一小節(jié), 我們將就Go語(yǔ)言標(biāo)準(zhǔn)包中的XML相關(guān)處理的包進(jìn)行介紹。

這個(gè)小節(jié)不會(huì)涉及XML規(guī)范相關(guān)的內(nèi)容(如需了解相關(guān)知識(shí)請(qǐng)參考其他文獻(xiàn)),而是介紹如何用Go語(yǔ)言來(lái)編解碼XML文件相關(guān)的知識(shí)。

假如你是一名運(yùn)維人員,你為你所管理的所有服務(wù)器生成了如下內(nèi)容的xml的配置文件:

<?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文檔描述了兩個(gè)服務(wù)器的信息,包含了服務(wù)器名和服務(wù)器的IP信息,接下來(lái)的Go例子以此XML描述的信息進(jìn)行操作。

解析XML

如何解析如上這個(gè)XML文件呢? 我們可以通過(guò)xml包的Unmarshal函數(shù)來(lái)達(dá)到我們的目的

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

data接收的是XML數(shù)據(jù)流,v是需要輸出的結(jié)構(gòu),定義為interface,也就是可以把XML轉(zhuǎn)換為任意的格式。我們這里主要介紹struct的轉(zhuǎn)換,因?yàn)閟truct和XML都有類似樹(shù)結(jié)構(gòu)的特征。

示例代碼如下:

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本質(zhì)上是一種樹(shù)形的數(shù)據(jù)格式,而我們可以定義與之匹配的go 語(yǔ)言的 struct類型,然后通過(guò)xml.Unmarshal來(lái)將xml中的數(shù)據(jù)解析成對(duì)應(yīng)的struct對(duì)象。如上例子輸出如下數(shù)據(jù)

{{ 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文件解析成對(duì)應(yīng)的struct對(duì)象是通過(guò)xml.Unmarshal來(lái)完成的,這個(gè)過(guò)程是如何實(shí)現(xiàn)的?可以看到我們的struct定義后面多了一些類似于xml:"serverName"這樣的內(nèi)容,這個(gè)是struct的一個(gè)特性,它們被稱為 struct tag,它們是用來(lái)輔助反射的。我們來(lái)看一下Unmarshal的定義:

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

我們看到函數(shù)定義了兩個(gè)參數(shù),第一個(gè)是XML數(shù)據(jù)流,第二個(gè)是存儲(chǔ)的對(duì)應(yīng)類型,目前支持struct、slice和string,XML包內(nèi)部采用了反射來(lái)進(jìn)行數(shù)據(jù)的映射,所以v里面的字段必須是導(dǎo)出的。Unmarshal解析的時(shí)候XML元素和字段怎么對(duì)應(yīng)起來(lái)的呢?這是有一個(gè)優(yōu)先級(jí)讀取流程的,首先會(huì)讀取struct tag,如果沒(méi)有,那么就會(huì)對(duì)應(yīng)字段名。必須注意一點(diǎn)的是解析的時(shí)候tag、字段名、XML元素都是大小寫(xiě)敏感的,所以必須一一對(duì)應(yīng)字段。

Go語(yǔ)言的反射機(jī)制,可以利用這些tag信息來(lái)將來(lái)自XML文件中的數(shù)據(jù)反射成對(duì)應(yīng)的struct對(duì)象,關(guān)于反射如何利用struct tag的更多內(nèi)容請(qǐng)參閱reflect中的相關(guān)內(nèi)容。

解析XML到struct的時(shí)候遵循如下的規(guī)則:

  • 如果struct的一個(gè)字段是string或者[]byte類型且它的tag含有",innerxml",Unmarshal將會(huì)將此字段所對(duì)應(yīng)的元素內(nèi)所有內(nèi)嵌的原始xml累加到此字段上,如上面例子Description定義。最后的輸出是

    <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中有一個(gè)叫做XMLName,且類型為xml.Name字段,那么在解析的時(shí)候就會(huì)保存這個(gè)element的名字到該字段,如上面例子中的servers。

  • 如果某個(gè)struct字段的tag定義中含有XML結(jié)構(gòu)中element的名稱,那么解析的時(shí)候就會(huì)把相應(yīng)的element值賦值給該字段,如上servername和serverip定義。
  • 如果某個(gè)struct字段的tag定義了中含有",attr",那么解析的時(shí)候就會(huì)將該結(jié)構(gòu)所對(duì)應(yīng)的element的與字段同名的屬性的值賦值給該字段,如上version定義。
  • 如果某個(gè)struct字段的tag定義 型如"a>b>c",則解析的時(shí)候,會(huì)將xml結(jié)構(gòu)a下面的b下面的c元素的值賦值給該字段。
  • 如果某個(gè)struct字段的tag定義了"-",那么不會(huì)為該字段解析匹配任何xml數(shù)據(jù)。
  • 如果struct字段后面的tag定義了",any",如果他的子元素在不滿足其他的規(guī)則的時(shí)候就會(huì)匹配到這個(gè)字段。
  • 如果某個(gè)XML元素包含一條或者多條注釋,那么這些注釋將被累加到第一個(gè)tag含有",comments"的字段上,這個(gè)字段的類型可能是[]byte或string,如果沒(méi)有這樣的字段存在,那么注釋將會(huì)被拋棄。

上面詳細(xì)講述了如何定義struct的tag。 只要設(shè)置對(duì)了tag,那么XML解析就如上面示例般簡(jiǎn)單,tag和XML的element是一一對(duì)應(yīng)的關(guān)系,如上所示,我們還可以通過(guò)slice來(lái)表示多個(gè)同級(jí)元素。

注意: 為了正確解析,go語(yǔ)言的xml包要求struct定義中的所有字段必須是可導(dǎo)出的(即首字母大寫(xiě))

輸出XML

假若我們不是要解析如上所示的XML文件,而是生成它,那么在go語(yǔ)言中又該如何實(shí)現(xiàn)呢? xml包中提供了MarshalMarshalIndent兩個(gè)函數(shù),來(lái)滿足我們的需求。這兩個(gè)函數(shù)主要的區(qū)別是第二個(gè)函數(shù)會(huì)增加前綴和縮進(jìn),函數(shù)的定義如下所示:

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

兩個(gè)函數(shù)第一個(gè)參數(shù)是用來(lái)生成XML的結(jié)構(gòu)定義類型數(shù)據(jù),都是返回生成的XML數(shù)據(jù)流。

下面我們來(lái)看一下如何輸出如上的XML:

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

上面的代碼輸出如下信息:

<?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>

和我們之前定義的文件的格式一模一樣,之所以會(huì)有os.Stdout.Write([]byte(xml.Header)) 這句代碼的出現(xiàn),是因?yàn)?code>xml.MarshalIndent或者xml.Marshal輸出的信息都是不帶XML頭的,為了生成正確的xml文件,我們使用了xml包預(yù)定義的Header變量。

我們看到Marshal函數(shù)接收的參數(shù)v是interface{}類型的,即它可以接受任意類型的參數(shù),那么xml包,根據(jù)什么規(guī)則來(lái)生成相應(yīng)的XML文件呢?

  • 如果v是 array或者slice,那么輸出每一個(gè)元素,類似value
  • 如果v是指針,那么會(huì)Marshal指針指向的內(nèi)容,如果指針為空,什么都不輸出
  • 如果v是interface,那么就處理interface所包含的數(shù)據(jù)
  • 如果v是其他數(shù)據(jù)類型,就會(huì)輸出這個(gè)數(shù)據(jù)類型所擁有的字段信息

生成的XML文件中的element的名字又是根據(jù)什么決定的呢?元素名按照如下優(yōu)先級(jí)從struct中獲?。?/p>

  • 如果v是struct,XMLName的tag中定義的名稱
  • 類型為xml.Name的名叫XMLName的字段的值
  • 通過(guò)struct中字段的tag來(lái)獲取
  • 通過(guò)struct的字段名用來(lái)獲取
  • marshall的類型名稱

我們應(yīng)如何設(shè)置struct 中字段的tag信息以控制最終xml文件的生成呢?

  • XMLName不會(huì)被輸出
  • tag中含有"-"的字段不會(huì)輸出
  • tag中含有"name,attr",會(huì)以name作為屬性名,字段值作為值輸出為這個(gè)XML元素的屬性,如上version字段所描述
  • tag中含有",attr",會(huì)以這個(gè)struct的字段名作為屬性名輸出為XML元素的屬性,類似上一條,只是這個(gè)name默認(rèn)是字段名了。
  • tag中含有",chardata",輸出為xml的 character data而非element。
  • tag中含有",innerxml",將會(huì)被原樣輸出,而不會(huì)進(jìn)行常規(guī)的編碼過(guò)程
  • tag中含有",comment",將被當(dāng)作xml注釋來(lái)輸出,而不會(huì)進(jìn)行常規(guī)的編碼過(guò)程,字段值中不能含有"--"字符串
  • tag中含有"omitempty",如果該字段的值為空值那么該字段就不會(huì)被輸出到XML,空值包括:false、0、nil指針或nil接口,任何長(zhǎng)度為0的array, slice, map或者string
  • tag中含有"a>b>c",那么就會(huì)循環(huán)輸出三個(gè)元素a包含b,b包含c,例如如下代碼就會(huì)輸出

    FirstName string   `xml:"name>first"`
    LastName  string   `xml:"name>last"`
    
    <name>
    <first>Asta</first>
    <last>Xie</last>
    </name>

上面我們介紹了如何使用Go語(yǔ)言的xml包來(lái)編/解碼XML文件,重要的一點(diǎn)是對(duì)XML的所有操作都是通過(guò)struct tag來(lái)實(shí)現(xiàn)的,所以學(xué)會(huì)對(duì)struct tag的運(yùn)用變得非常重要,在文章中我們簡(jiǎn)要的列舉了如何定義tag。更多內(nèi)容或tag定義請(qǐng)參看相應(yīng)的官方資料。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)