Golang XML自定义输出

Golang XML自定义输出

问题描述:

I'm trying to create an XML implementing the MarshalXML output. But currently i'm facing several issues.

The structure i'm using for storing the data is:

type Edition struct {
    Launch         string             `xml:"launch" json:"launch"`
    Code           string             `xml:"code" json:"code"`
    Names          []NameNode         `xml:"names>name"`
    Cards          CardsComposition   `xml:"cards" json:"cards,omitempty"`
    Preconstructed PreconstructedInfo `xml:"preconstructed" json:"preconstructed,omitempty"`
    Vault          *struct{}          `xml:"vault" json:"vault"`
    Online         *struct{}          `xml:"online" json:"online"`
}

What i want is: If the Preconstructed field is not set, don't put the <preconstructed> tag (using the standard marshaler it put it even if it is empty).

So what i did is:

func (preconstructed PreconstructedInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
    if (PreconstructedInfo{} == preconstructed) {
        return nil
    }
    return e.EncodeElement(preconstructed, start)
}

And it apparently works, if I use it for encoding a single Edition entity. But if I try to encode an array of Edition entities, I get the following error:

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

(the array is ~200 entries)

So what I don't understand is:

  • Why the stack overflow issue happens only when I try to customize the xml, that in this case is also trying to remove empty tags, so "saving space"
  • What is the best way to do it? And someone can explain me how to implement a custom XML Marshaler for go? I found plenty for JSON marshal, but nearly nothing for XML)

Ok, i'll answer myself since i finally solved that issue.

So apparently one of the problem is that EncodeElement is making use of MarshalXML, and with a huge file it cause an explosion function calls.

Anyway the solution is to manually encode all components of the element.

So in that case i did that:

// MarshalXML generate XML output for PrecsontructedInfo
func (preconstructed PreconstructedInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
    if (PreconstructedInfo{} == preconstructed) {
        return nil
    }
    if preconstructed.Decks > 0 {
        start.Attr = []xml.Attr{xml.Attr{Name: xml.Name{Local: "decks"}, Value: strconv.Itoa(preconstructed.Size)}}
    }
    if strings.Compare(preconstructed.Type, "") != 0 {
        start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "type"}, Value: preconstructed.Type})
    }

    err = e.EncodeToken(start)
    e.EncodeElement(preconstructed.Size, xml.StartElement{Name: xml.Name{Local: "size"}})
    return e.EncodeToken(xml.EndElement{Name: start.Name})
}

So what i did is:

  1. Check if the field is empty or not, if yes return null (the same as in my question)
  2. If not empty, check for the values contained in PreconstructedInfo, and add them in their relevant position, add first attributes to the start element. start.Attr will contain the xml attributes for the tag being Marshalled, the syntax for it is quite easy, you specify the name and the value. This has to be done before calling e.EncodeToken(start).
  3. After that encode the other elements of the tag into the current start Element. As you can see you must encode the tag using the xml.StartElement, in a similar way as for the attribute.
  4. Finally you can close the start Tag.

In that way it will generate the xml tag only if data is available, and add attributes/childs only if they have a value, if they are empty or 0 it will not be added.