Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Go, encoding/xml: How can I marshal self-closing elements?

Tags:

xml

go

I'm writing XML from the following struct:

type OrderLine struct {
    LineNumber     string `xml:"LineNumber"`
    Product        string `xml:"Product"`
    Ref            string `xml:"Ref"`
    Quantity       string `xml:"Quantity"`
    Price          string `xml:"Price"`
    LineTotalGross string `xml:"LineTotalGross"`
}

If the Ref field is empty, I'd like the element to display, but be self-closing, i.e.

<Ref />

and not:

<Ref></Ref>

AFAIK, these two are semantically equivalent, but I would prefer a self-closing tag, as it matches the output from other systems. Is this possible?

like image 605
Ross McFarlane Avatar asked Dec 01 '25 02:12

Ross McFarlane


1 Answers

This post provides two solutions that do not rely on regexp and explains the differences between the two.

The first version is memory friendly, but cpu adverse. It implements a writer that replaces occurrences of search by replace within buffered bytes. It tries to write data as soon as possible, preventing large allocation in memory. It is not the best usage of the cpu because if will scan same data multiple times.

package main

import (
    "bytes"
    "encoding/xml"
    "fmt"
    "io"
    "os"
)

// Person represents a <person> node in the XML
type Person struct {
    XMLName   xml.Name   `xml:"Players"`
    DataItems []dataItem `xml:"DataItem"`
}

// Skill represents a <skill> node in the XML
type dataItem struct {
    XMLName        xml.Name `xml:"DataItem"`
    Name           string   `xml:"skillName,attr"`
    YearsPracticed int64    `xml:"practice,attr"`
    Level          string   `xml:"level,attr"`
}

func main() {
    players := Person{
        DataItems: []dataItem{
            {Name: "Soccer", YearsPracticed: 3, Level: "Newbie"},
            {Name: "Basketball", YearsPracticed: 4, Level: "State"},
            {Name: "Baseball", YearsPracticed: 10, Level: "National"},
        },
    }
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)

    dst := &Replace{
        Writer:  os.Stdout,
        Search:  []byte("></DataItem>"),
        Replace: []byte("/>"),
    }
    defer dst.Flush()

    enc := xml.NewEncoder(dst)
    enc.Indent("", "  ")
    if err := enc.Encode(players); err != nil {
        fmt.Printf("error: %v\n", err)
    }
}

type Replace struct {
    io.Writer
    Search  []byte
    Replace []byte
    buf     []byte
}

func (s *Replace) Write(b []byte) (n int, err error) {
    s.buf = append(s.buf, b...)
    s.buf = bytes.ReplaceAll(s.buf, s.Search, s.Replace)
    if len(s.buf) > len(s.Search) {
        w := s.buf[:len(s.buf)-len(s.Search)]
        n, err = s.Writer.Write(w)
        s.buf = s.buf[n:]
    }
    return len(b), err
}

func (s *Replace) Flush() (err error) {
    var n int
    n, err = s.Writer.Write(s.buf)
    s.buf = s.buf[n:]
    return
}

The second version is cpu friendly but memory adverse, as it loads the entire data to modify in memory.

package main

import (
    "bytes"
    "encoding/xml"
    "fmt"
    "os"
)

// Person represents a <person> node in the XML
type Person struct {
    XMLName   xml.Name   `xml:"Players"`
    DataItems []dataItem `xml:"DataItem"`
}

// Skill represents a <skill> node in the XML
type dataItem struct {
    XMLName        xml.Name `xml:"DataItem"`
    Name           string   `xml:"skillName,attr"`
    YearsPracticed int64    `xml:"practice,attr"`
    Level          string   `xml:"level,attr"`
}

func main() {
    players := Person{
        DataItems: []dataItem{
            {Name: "Soccer", YearsPracticed: 3, Level: "Newbie"},
            {Name: "Basketball", YearsPracticed: 4, Level: "State"},
            {Name: "Baseball", YearsPracticed: 10, Level: "National"},
        },
    }
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)

    out := new(bytes.Buffer)

    enc := xml.NewEncoder(out)
    enc.Indent("", "  ")
    if err := enc.Encode(players); err != nil {
        fmt.Printf("error: %v\n", err)
    }

    b := bytes.ReplaceAll(out.Bytes(), []byte("></DataItem>"), []byte("/>"))
    os.Stdout.Write(b)
}

Choose one according to your context of execution.


Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!