Search code examples
xmlgoxml-encoding

XML Encoder in Golang does not close tags in all streams


I'm working on a streaming XML encoder that will simultaneously write the XML to a local file, and an S3 bucket. However, just by testing it writing to two local files, I can see that one of the files is missing the closing tags every time.

Here is roughly how I do it (omitting error handling):

func copyToFile (fileName string) {
    f, _ := os.Create(fileName)
    defer f.Close()
    io.Copy(f, pr)
}

func main () {
    pr, pw := io.Pipe()
    defer pw.Close()

    encoder := xml.NewEncoder(pw)

    go copyToFile("file1.xml")
    go copyToFile("file2.xml")

    encoder.EncodeToken(xml.StartElement{...})
    encoder.Encode(SomeStruct{})
    encoder.EncodeToken(xml.EndElement{...})
    encoder.Flush()
}

The result in file1.xml is as expected, with all tags properly closed, but in file2.xml the closing tag (the call of encoder.EncodeToken(xml.EndElement{...})) is missing.

What am I doing wrong? Can I expect the same result when I copy the reader to S3?


Solution

  • You cannot have multiple readers on the returned io.PipeReader, data will not be duplicated for all readers. The io.PipeReader can only "serve" a single reader, and you launch 2 goroutines to read from it.

    To achieve what you want, use io.MultiWriter(). It returns you a single io.Writer to where you can write, and it will replicate the writes to all the writers you pass to it.

    For example:

    f1 := &bytes.Buffer{}
    f2 := &bytes.Buffer{}
    
    w := io.MultiWriter(f1, f2)
    
    encoder := xml.NewEncoder(w)
    
    encoder.EncodeToken(xml.StartElement{Name: xml.Name{Local: "test"}})
    encoder.Encode(image.Point{1, 2})
    encoder.EncodeToken(xml.EndElement{Name: xml.Name{Local: "test"}})
    encoder.Flush()
    
    fmt.Println(f1)
    fmt.Println(f2)
    

    This will output (try it on the Go Playground):

    <test><Point><X>1</X><Y>2</Y></Point></test>
    <test><Point><X>1</X><Y>2</Y></Point></test>
    

    The above example writes to 2 in-memory buffers. To write to 2 files, you may pass 2 os.Files to io.MultiWriter() (or anything else that implements io.Writer):

    f1, err := os.Create("file1.xml")
    if err != nil {
        panic(err)
    }
    defer f1.Close()
    
    f2, err := os.Create("file2.xml")
    if err != nil {
        panic(err)
    }
    defer f2.Close()
    
    w := io.MultiWriter(f1, f2)
    
    // ...