imjoey / blog

My Blog based on Github Issues.
286 stars 44 forks source link

定制实现 Go 中的 XML Unmarshal - 基础篇 #19

Open imjoey opened 7 years ago

imjoey commented 7 years ago

前言

由于 oVirt 的 API 接口的数据格式是 XML,所以在实现 oVirt Go SDK 时,需要对接口响应 XML 数据进行解析。接口的XML 数据格式非常标准,所以非常适合用 xml:"***" struct tag 来实现 Unmarshal 操作。Go 自带的 encoding/xml 库提供了非常便捷的 XML Marshal 和 Unmarshal 功能,只需在 struct 中定义 tag 即可。

但后来在完善 SDK 的过程中,我发现了 struct tag 方案的一些局限性,于是决定自行实现 SDK 中的 XML-Unmarshal。

由于 SDK 中 XML 数据的特殊性,所以实现方案是根据 XML 数据定制的,并不具有通用性。

encoding/xml Unmarshal 的局限

无法判断 XML 与 struct 对应

encoding/xml.Unmarshal 方法返回一个 error,但当 XML 数据与传入的 struct 不对应时,返回的 error 仍然是 nil,以下面为例:Playground

package main

import "fmt"
import "encoding/xml"

var data string = `
<table>
    <name>
        <code>23764</code>
        <name>Smith, Jane</name>
    </name>
    <name>
        <code>11111</code>
        <name>Doe, John</name>
    </name>
</table>
`

type Customer struct {
    Abc string `xml:"abc"`   // tag 应为 "code" 或 "name"
    Bcd string `xml:"bcd"`   // tag 应为 "code" 或 "name"
}

type Customers struct {
    Customers []Customer `xml:"custttt"`    // tag 应为 "name"
}

func main() {
    var custs Customers
    err := xml.Unmarshal([]byte(data), &custs)

    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(custs)    // 输出结果是 {[]}(就是 custs 的零值)
}

上面的例子中,Customers 和 Customer 中的 tag 定义与 XML 中的 element 定义完全不一样,但xml.Unmarshal 函数不返回任何错误,custs 仍然是零值。

因为,xml.Unmarshal 自动忽略不认识的 XML 标签值。

这种情况导致,当 XML 数据可能是两种完全不同的格式(即对应两个完全不同的 struct)时,无法判断到底是哪个。我尝试了一个解决方案,但很丑陋,而且还不能保证完全正确,即:

除了变量值外,再定义一个零值,然后 Unmarshal 完对比一下二者是否相等,如果相等,则大概率 XML 与 struct 不匹配;如果不相等,则肯定有一部分是匹配的。

struct 的属性必须 exported

由于 encoding/xml 使用 reflect 反射来实现 Unmarshal,所以要求 struct 的属性都必须是 exported 的,比如:对于 XML 中的标签<name>而言,对应的属性一般定义为 Name。在我实现 oVirt Go SDK 时,就出现了问题:

所以这就导致 unexported 的属性是无法使用 xml.Unmarshal的;即便可以继续使用 Name 作为属性名,但getter 函数只能用 GetName ,但这样不符合 Go 的编码规范。

关于 Go XML Stream API

对于 Go 中的 XML Stream API,官方和网上的资料很少,因为绝大多数的 XML Unmarshal 都是直接使用上面提到的 struct tag 方式,找不到可以直接参考的例子。

通过查看 encoding/xml 中的marshal.goread.goxml.go三个源文件,找到了一些基础概念和简单操作。

下面使用一段代码来描述 Go XML Stream API 的简单用法。playgroud

package main

import (
    "bytes"
    "encoding/xml"
    "fmt"
    "io"
)

var xmlstring = `
        <Person>
            <FullName>Grace R. Emlin</FullName>
            <!-- this is the comment for Company -->
            <Company>Example Inc.</Company>
            <City>Hanga Roa</City>
            <State>Easter Island</State>
        </Person>
    `

func main() {
    decoder := xml.NewDecoder(bytes.NewReader([]byte(xmlstring)))
    for {
        t, err := decoder.Token()
        if err != nil {
            if err == io.EOF {
                fmt.Printf("Parse XML finished!\n")
            } else {
                fmt.Printf("Failed to Parse XML with the error of %v\n", err)
            }
            break
        }
        t = xml.CopyToken(t)
        switch t := t.(type) {
        case xml.StartElement:
            fmt.Printf("StartElement: <%v>\n", t.Name.Local)
        case xml.EndElement:
            fmt.Printf("EndElement: <%v>\n", t.Name.Local)
        case xml.CharData:
            fmt.Printf("CharData: %v\n", string(t))
        case xml.Comment:
            fmt.Printf("Comment: <!--%v-->\n", string(t))
        }
    }
}

输出是:


CharData: 

StartElement: <Person>
CharData: 

StartElement: <FullName>
CharData: Grace R. Emlin
EndElement: <FullName>
CharData: 

Comment: <!-- this is the comment for Company -->
CharData: 

StartElement: <Company>
CharData: Example Inc.
EndElement: <Company>
CharData: 

StartElement: <City>
CharData: Hanga Roa
EndElement: <City>
CharData: 

StartElement: <State>
CharData: Easter Island
EndElement: <State>
CharData: 

EndElement: <Person>
CharData: 

Parse XML finished!

根据输出可以看到,原 XML 中的换行符被 Go XML Stream API 解析为独立的xml.Token(实质是:xml.CharData),这是需要特别注意的地方。

总结

本文简单介绍了我要定制实现 XML Unmarshal 的原因,和 Go XML Stream API 的一些基础,后续在我完成 oVirt Go SDK 的 XML Unmarshal 重构后,会将更详细的实现细节补充上来。