在接口开发和数据迁移场景中,XML和JSON的互相转换是一个高频需求。Go语言的标准库设计得非常优雅,encoding/xml负责XML的序列化,encoding/json负责JSON的序列化,两者都支持通过struct标签来映射字段。把这两者组合起来,就能实现XML到JSON的转换,而把结构体作为参数传入通用函数,则可以让转换逻辑只写一次就服务所有数据模型,大幅减少重复代码。本文将从基础做法讲到反射进阶,完整拆解这套技巧。

一、基础方案:定义结构体配合双重标签完成转换
Go语言处理结构化数据的核心思想是“结构体加标签”。同一个结构体可以同时携带XML标签和JSON标签,这样XML数据解码进来之后,同一个内存对象可以直接编码成JSON输出。这是最直观也是最推荐的方案,代码清晰、性能好、类型安全。
假设有一段用户信息的XML需要转换成JSON,我们先定义对应的结构体:
// User 同时定义了 xml 和 json 两套标签
// xml 标签用于从XML解析数据,json 标签用于输出JSON
type User struct {
XMLName xml.Name `xml:"user" json:"-"`
ID int `xml:"id,attr" json:"id"`
Name string `xml:"name" json:"name"`
Email string `xml:"email" json:"email,omitempty"`
}
func xmlToJson(xmlData []byte) ([]byte, error) {
var user User
// 第一步:把XML字节流反序列化到结构体
if err := xml.Unmarshal(xmlData, &user); err != nil {
return nil, err
}
// 第二步:把结构体序列化为JSON字节流
return json.Marshal(user)
}
这段代码中有几个细节需要注意。首先是XMLName字段,它用来对应XML的根元素名,json标签写成-表示不输出到JSON结果中。其次,xml:"id,attr"中的attr表示这个字段映射的是XML元素的属性而不是子元素。最后,omitempty可以让空字符串字段在JSON中省略,避免输出一堆无意义的空值。
调用方式很简单,传入XML字节流即可得到JSON:
func main() {
xmlData := []byte(`<user id="1001"><name>张三</name><email>zhangsan@ipipp.com</email></user>`)
jsonData, err := xmlToJson(xmlData)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData))
// 输出: {"id":1001,"name":"张三","email":"zhangsan@ipipp.com"}
}
二、核心技巧:把结构体作为参数实现通用转换函数
上面的做法有一个明显缺陷:xmlToJson函数把User类型写死了,每来一种新的XML结构就要再写一个函数。解决办法是把结构体实例作为参数传入,函数内部通过空接口interface{}接收任意类型,再用标准库的Unmarshal函数把XML解码到这个实例中。这就是标题所说的“结构体作为参数的技巧”。
通用函数的写法如下:
// ConvertXMLToJSON 接收一个结构体指针作为目标容器
// data 是原始XML字节流,v 必须是可寻址的指针类型
func ConvertXMLToJSON(data []byte, v interface{}) ([]byte, error) {
// 通过反射检查传入的是否为指针类型
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return nil, errors.New("参数必须是有效的结构体指针")
}
// 把XML解码到调用方提供的结构体实例中
if err := xml.Unmarshal(data, v); err != nil {
return nil, fmt.Errorf("解析XML失败: %w", err)
}
// 直接将结构体编码为JSON
return json.Marshal(v)
}
这个函数的巧妙之处在于,xml.Unmarshal和json.Marshal的第二个参数本身就是interface{}类型,标准库内部已经用反射做了动态分派,我们只需要把调用方传入的结构体指针原样传递即可。这样一来,无论是用户、订单还是商品数据,统统使用同一个转换函数:
type Order struct {
XMLName xml.Name `xml:"order" json:"-"`
OrderNo string `xml:"no,attr" json:"order_no"`
Amount float64 `xml:"amount" json:"amount"`
Items []Item `xml:"item" json:"items"`
}
type Item struct {
Product string `xml:"product,attr" json:"product"`
Count int `xml:"count,attr" json:"count"`
}
func main() {
xmlData := []byte(`<order no="A20240001"><amount>199.5</amount>` +
`<item product="键盘" count="2"/><item product="鼠标" count="1"/></order>`)
var order Order
result, err := ConvertXMLToJSON(xmlData, &order)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(result))
}
需要强调一个关键点:传入的必须是结构体的指针而不是值。因为xml.Unmarshal需要修改结构体内部的字段,如果传值,Go会复制一份副本,解码结果无法带回调用方。上面的函数用reflect.Ptr做了前置校验,可以在编译期之外提前拦截这类错误,避免出现解码成功但数据全空的诡异现象。
三、处理复杂XML结构:属性、CDATA与嵌套映射
真实业务中的XML往往比示例复杂得多,包含属性、CDATA包裹的文本块、多层嵌套甚至动态字段。encoding/xml包提供了一套完整的标签语法来应对这些情况,掌握它们是写出可靠转换的前提。
先看属性和CDATA的处理。XML中chardata表示元素的字符数据,cdata表示CDATA块,,attr表示元素属性:
type Article struct {
XMLName xml.Name `xml:"article" json:"-"`
Title string `xml:"title" json:"title"`
Content string `xml:"content>cdata" json:"content"` // 提取CDATA内容
Author string `xml:"author,attr" json:"author"`
Date string `xml:"date,attr" json:"date"`
}
func main() {
xmlData := []byte(`<article author="李四" date="2024-06-01">` +
`<title>Go进阶</title>` +
`<content><![CDATA[包含 <特殊> 字符的正文]]></content>` +
`</article>`)
var a Article
result, _ := ConvertXMLToJSON(xmlData, &a)
fmt.Println(string(result))
}
再看完全动态的场景。如果XML的字段名不固定,无法提前定义结构体,可以借助map[string]interface{}配合>any>标签,或者干脆使用xml.Decoder逐个token解析。下面的通用结构可以捕获任意子元素:
// DynamicDoc 用通配符捕获所有子节点,key为标签名,value为内容
type DynamicDoc struct {
XMLName xml.Name `xml:"root" json:"-"`
Fields map[string]interface{} `xml:",any" json:"-"`
}
// 由于 map 无法直接带标签映射,实际需要用 token 解析,此处演示思路
func parseAnyXML(data []byte) (map[string]interface{}, error) {
result := make(map[string]interface{})
decoder := xml.NewDecoder(bytes.NewReader(data))
var currentKey string
for {
tok, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
switch t := tok.(type) {
case xml.StartElement:
currentKey = t.Name.Local
case xml.CharData:
if currentKey != "" {
result[currentKey] = strings.TrimSpace(string(t))
}
}
}
return result, nil
}
token级别的解析虽然繁琐,但灵活性最高,配合前面的通用转换函数,可以再封装一层:先用token解析得到map,再对map做JSON序列化,这样连结构体都不需要定义,适合做XML到JSON的快速预览工具或数据中转服务。
四、常见坑点与性能优化建议
第一,XML和JSON的字段命名习惯不同。XML常用驼峰或全小写,JSON社区更偏好snake_case,所以在写标签时不要偷懒复用同一个名字,显式写两套标签可以避免后期接口对接时的字段名冲突。
第二,XMLName字段如果不加json:"-",输出结果里会多出一个{"XMLName":{...}}的怪异结构,这是新手最容易踩的坑。第三,XML解析对命名空间不友好,如果文档带有xmlns前缀,标签需要写成xml:"ns:element"的形式,或者用xml.Name类型的字段单独接收命名空间信息。
性能方面,xml.Unmarshal每次都要新建解码器,在高并发场景下可以用xml.Decoder配合对象池复用结构体实例,减少内存分配。JSON编码时如果追求极致性能,可以换用json.Encoder流式输出,或者选择sonic等高性能第三方库。对于大部分业务系统来说,标准库的性能已经足够,优先保证代码的正确性和可维护性才是更明智的选择。