Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
imjoey opened this issue Jul 31, 2017 · 0 comments
Open

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

imjoey opened this issue Jul 31, 2017 · 0 comments
Labels

Comments

@imjoey
Copy link
Owner

imjoey commented Jul 31, 2017

前言

由于 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 时,就出现了问题:

  • oVirt 社区希望每个 struct(type)的属性是 unexported 的,也就是使用小写字母开头,即 name 作为属性名,访问和设值则使用 getter/setter 函数
  • Go 编码规范中,一般使用 Name() 作为 getter函数,用 SetName()作为 setter 函数

所以这就导致 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三个源文件,找到了一些基础概念和简单操作。

  • xml.Decoder 代表了一个XML的 Stream,xml.Decoder.Token() 函数即返回 XML Stream 中的下一个元素,返回值是 xml.Token
  • xml.Token 代表了 XML 中每一项元素,会被解析成 xml.StartElementxml.EndElementxml.CharDataxml.Commentxml.ProcInstxml.Directive 中的一种
  • 不同于 Java 的 XML Stream API,XML 标签的内容会被 Go 解析为独立的 xml.CharData,即 xml.Token 中的一种

下面使用一段代码来描述 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 重构后,会将更详细的实现细节补充上来。

@imjoey imjoey changed the title Go 定制实现 Go 中的 XML unmarshal Jul 31, 2017
@imjoey imjoey added the Go label Jul 31, 2017
@imjoey imjoey changed the title 定制实现 Go 中的 XML unmarshal 定制实现 Go 中的 XML Unmarshal Jul 31, 2017
@imjoey imjoey changed the title 定制实现 Go 中的 XML Unmarshal 定制实现 Go 中的 XML Unmarshal - 基础篇 Jul 31, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant