不能直接用xml.Unmarshal + json.Marshal,因为XML常含动态标签、混合内容及属性与文本共存,硬套struct会丢数据或panic;Go标准库无内置“XML→通用map→JSON”链路,需手动递归解析token流并处理属性前缀、同名节点合并等细节。

为什么不能直接用 xml.Unmarshal + json.Marshal?
很多人第一反应是先解析 XML 到 struct,再序列化为 JSON。这在结构已知、字段固定时可行,但实际中 XML 常含动态标签、混合内容、属性与文本共存(比如 <item id="123">hello</item>),硬套 struct 会丢数据或 panic。Go 标准库不提供“XML → 通用 map → JSON”的内置链路,必须自己桥接。
用 encoding/xml 解析为通用 map[string]interface{} 的关键点
核心是递归遍历 xml.Token 流,把开始标签、字符数据、结束标签组装成嵌套 map。注意三点:
-
xml.StartElement的Name.Local是标签名,Attr是属性列表,需转为map[string]string并加前缀(如@id)避免和子元素冲突 - 同名兄弟节点(如多个
<tag>...)必须合并为 slice,不能覆盖——否则只留最后一个 - 字符数据(
xml.CharData)若非空且父节点无子元素,才作为#text字段存入;否则忽略(防止空白符污染)
示例片段(简化版):
// node 是当前层级 map,tok 是 xml.Token
switch t := tok.(type) {
case xml.StartElement:
child := make(map[string]interface{})
for _, a := range t.Attr {
child["@"+a.Name.Local] = a.Value
}
stack = append(stack, node)
node[t.Name.Local] = child
current = child
case xml.CharData:
s := strings.TrimSpace(string(t))
if s != "" && len(current) == 0 {
current["#text"] = s
}
}
属性命名冲突和空值处理怎么避坑?
XML 属性和子元素同名很常见(如 <user id="1"><id>2</id></user>),直接平铺会覆盖。标准做法是:
- 所有属性加
@前缀(@id),子元素保持原名(id) - 纯文本内容统一用
#text键,不与标签名冲突 - 空元素(
<empty/>)生成{"#text": ""}或null,取决于业务——建议保留空字符串,避免 JSON 解析时类型不一致 - 如果原始 XML 有 namespace(如
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"),xml.Token默认忽略,除非你显式调用t.Copy()并检查Namespace字段
性能瓶颈在哪?有没有更轻量的替代方案?
逐 token 构建 map 是最灵活的方式,但 GC 压力大(大量临时 map 和 string 分配)。如果 XML 规则简单(无混合内容、无重复标签、属性少),可考虑正则预处理 + json.RawMessage 绕过解析:
- 用
regexp.ReplaceAllStringFunc把<tag attr="v">替成"tag": {"@attr":"v", "#text":,再补右括号——仅限可控输入 - 对超大文件(>50MB),用
xml.Decoder的Token()配合 streaming JSON encoder(如github.com/json-iterator/go)边读边写,避免全量内存驻留 - 别用
gob或yaml中间格式转换,徒增开销
真正难的是混合内容(<p>Hello <em>world</em>.</p>)——这时必须保留 token 序列,map 结构无法表达顺序,只能退回到自定义 AST 节点。


















