20

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?

2
  • 4
    I think this go-nuts thread discusses same thing. I have doubt golang has support of what you are asking. groups.google.com/forum/#!topic/golang-nuts/guG6iOCRu08 Commented Jun 30, 2016 at 9:39
  • 1
    You can't. (Well, except just s,<Ref></Ref>,<Ref />,g.) Commented Jun 30, 2016 at 10:19

3 Answers 3

2

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.

Sign up to request clarification or add additional context in comments.

Comments

1

I found a way to do it "hacking" marshal package, but I didn't test it. If you want me to show you the link, let me now, then I post it in comments of this reply.

I did some manually code:

package main

import (
    "encoding/xml"
    "fmt"
    "regexp"
    "strings"
)

type ParseXML struct {
    Person struct {
        Name     string `xml:"Name"`
        LastName string `xml:"LastName"`
        Test     string `xml:"Abc"`
    } `xml:"Person"`
}

func main() {

    var err error
    var newPerson ParseXML

    newPerson.Person.Name = "Boot"
    newPerson.Person.LastName = "Testing"

    var bXml []byte
    var sXml string
    bXml, err = xml.Marshal(newPerson)
    checkErr(err)

    sXml = string(bXml)

    r, err := regexp.Compile(`<([a-zA-Z0-9]*)><(\\|\/)([a-zA-Z0-9]*)>`)
    checkErr(err)
    matches := r.FindAllString(sXml, -1)

    fmt.Println(sXml)

    if len(matches) > 0 {
        r, err = regexp.Compile("<([a-zA-Z0-9]*)>")
        for i := 0; i < len(matches); i++ {

            xmlTag := r.FindString(matches[i])
            xmlTag = strings.Replace(xmlTag, "<", "", -1)
            xmlTag = strings.Replace(xmlTag, ">", "", -1)
            sXml = strings.Replace(sXml, matches[i], "<"+xmlTag+" />", -1)

        }
    }

    fmt.Println("")
    fmt.Println(sXml)

}

func checkErr(chk error) {
    if chk != nil {
        panic(chk)
    }
}

1 Comment

I do not recommend this. Playing with the string after the fact is not a good idea.
0

Until go v1.20.3 it's official package "encoding/xml" still not support shortform, so I modified it to support auto close tag. You can find it here. https://github.com/ECUST-XX/xml

4 Comments

How does one use self-enclosing tags in go v1.20.3? I still can't figure it out
@TheNerdyGeek, I think it's an English language issue, perhaps misunderstanding how "until" works. What was meant was probably, "As of the latest Go version, which is 1.20.3 at the time of this answer, its official encoding/xml package still does not support short-form."
Thank you @RobKennedy for the clarification

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.