Go 跳过一个坏 XML 元素后,后面的记录全没了
自定义 UnmarshalXML 未读完元素便返回错误时,Decoder 可能留下内部 EOF 边界。把业务校验放在完整消费元素之后,解析错误则停止,避免外层盲目 Skip。
XML 有四条记录,第二条业务校验失败后调用 Decoder.Skip(),循环马上收到 io.EOF,第三、第四条都没有返回。若元素类型实现了 UnmarshalXML,先检查它是否在读完当前元素前就返回了错误。
Go 1.27.3 的对应实现会在调用自定义解码器前放入一个元素级 EOF 边界。自定义方法未消费完整元素便退出时,这个标记可能无法被正常移除;外层再 Skip,反而把旧标记暴露出来。此时的 EOF 不能证明原文档已经完整读完,嵌套解码还可能发生层级错位。
在同一个方法里完成消费和业务判断
不要在外层对所有 DecodeElement 错误统一 Skip。调用者通常不知道自定义方法已经读到了哪一层,Skip 可能只跳过最内层尚未闭合的元素。
对于单条记录,可以先解到不实现 UnmarshalXML 的临时结构,完整消费后再做业务校验。下面只展示这一模式,实际字段按原 XML 契约保留:
var ErrBadItem = errors.New("invalid item value")
type Item struct {
Value string
}
func (item *Item) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var wire struct {
Value string `xml:"v,attr"`
}
if err := d.DecodeElement(&wire, &start); err != nil {
return err
}
if wire.Value == "bad" {
return ErrBadItem
}
item.Value = wire.Value
return nil
}
片段需要导入 errors 和 encoding/xml。临时结构没有自定义解码方法,避免再次递归调用自己。这里的 ErrBadItem 只在元素已经完整读取后返回;外层若按业务允许跳过该记录,应识别这类明确的业务错误并直接继续,不再额外 Skip。
如果临时结构的 DecodeElement 本身失败,结构可能尚未读完,应停止本次文档处理并保留输入;不要把语法错误、输入截断与完整读取后的业务拒绝合并处理。业务需要保留未知字段或其他子元素时,临时结构也应遵守原来的消费契约,不能为了此模式无意丢弃必需数据。
用末尾记录和嵌套结构验收
在可控输入上覆盖正常、业务无效、缺闭合标签和自定义方法提前退出。明确核对坏记录后仍有多少条、它们属于哪一层,不能只检查循环最后得到了 EOF。
应用提交结果前应保持既有的完整文档与记录数量校验,避免将半份数据当作成功导入。已有输出若可能丢尾,需要回到保留的原始输入核对,不凭读取端“没有新错误”认定数据齐全。
自定义解码器应明确完整读取自身元素,并保留业务拒绝与解析失败的区别。升级后仍用坏记录后的尾部数据和嵌套结构检查这条接口边界。


