Post

Writing to Multiple Destinations in Go with io.MultiWriter

The Go programming language, well-regarded for its simplicity and efficiency, has an extensive standard library containing a plethora of built-in packages. Among these, the io package plays a critical role due to its wide-ranging functionality, handling I/O operations. Today, our attention will be directed towards the MultiWriter type, an important feature of the io package.

What is MultiWriter?

MultiWriter is a type in the io package of Go that facilitates writing to multiple output destinations using a single write operation. This functionality is akin to the Unix tee command, enabling simultaneous writing to several Writers. The primary objective of MultiWriter is to reduce redundancy and enhance code readability by removing the need to call the write function multiple times for different output writers.

Here is the construction of the multiWriter:

1
2
3
type multiWriter struct {
	writers []Writer
}

Each multiWriter contains a slice of Writer types, denoting the multiple destinations where data should be written.

Why Use MultiWriter?

MultiWriter becomes an important tool in scenarios where you want to write the same data to multiple locations. For example, logging is a common use case. You may want to write log data both to a log file and the console at the same time.

Benefits of using MultiWriter include:

  1. Code Efficiency: Instead of writing separate blocks of code to handle each writer, you can use MultiWriter to write to multiple writers with a single write call, making your code more efficient and cleaner.
  2. Logging and Monitoring: MultiWriter is extremely useful in logging and monitoring processes. It allows for simultaneous writing of log or monitoring data to different destinations, such as a local log file, a network server, or standard output.

How Does MultiWriter Work?

The io.MultiWriter function in Go’s io package returns a writer that duplicates its writes to all provided writers, much like the Unix tee(1) command.

This is achieved through the use of the multiWriter structure and its methods.

Let’s break down the components of this structure:

  • writers: This is a slice of Writer interfaces. Each Writer in this slice is a destination where data will be written.
1
2
3
type multiWriter struct {
	writers []Writer
}

Write

1
2
3
4
5
6
7
8
9
10
11
12
13
func (t *multiWriter) Write(p []byte) (n int, err error) {
	for _, w := range t.writers {
		n, err = w.Write(p)
		if err != nil {
			return
		}
		if n != len(p) {
			err = ErrShortWrite
			return
		}
	}
	return len(p), nil
}

This method writes a byte slice to each writer in the writers slice, one at a time. It returns the number of bytes written and an error if one occurs. If a writer returns an error, the Write operation stops immediately and does not continue to the remaining writers in the list. This method also ensures that the complete byte slice is written to each writer - if not, it returns ErrShortWrite.

WriteString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (t *multiWriter) WriteString(s string) (n int, err error) {
	var p []byte // lazily initialized if/when needed
	for _, w := range t.writers {
		if sw, ok := w.(StringWriter); ok {
			n, err = sw.WriteString(s)
		} else {
			if p == nil {
				p = []byte(s)
			}
			n, err = w.Write(p)
		}
		if err != nil {
			return
		}
		if n != len(s) {
			err = ErrShortWrite
			return
		}
	}
	return len(s), nil
}

This method is similar to Write, but accepts a string instead of a byte slice. It optimizes the write operation by using the WriteString method of the StringWriter interface, if a writer supports it.

MultiWriter

The MultiWriter function uses the multiWriter struct to create a writer that writes to all provided writers:

1
2
3
4
5
6
7
8
9
10
11
func MultiWriter(writers ...Writer) Writer {
	allWriters := make([]Writer, 0, len(writers))
	for _, w := range writers {
		if mw, ok := w.(*multiWriter); ok {
			allWriters = append(allWriters, mw.writers...)
		} else {
			allWriters = append(allWriters, w)
		}
	}
	return &multiWriter{allWriters}
}

When MultiWriter is called, it creates a slice of writers. If any of the provided writers is itself a multiWriter, MultiWriter flattens it into its constituent writers, thus ensuring that each write operation is performed directly on the underlying writers and not on intermediate multiWriter instances. It then returns a multiWriter that writes to all these writers.

Using MultiWriter

Here’s an illustrative example on how to use MultiWriter, featuring writing an XML encoding of a slice of Person structs to multiple destinations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type Person struct {
	Name    string
	Surname string
}

func (p *Person) Write(b []byte) (n int, err error) {
	fmt.Printf("Write method ran. Name of person is %s \n", p.Name)
	return len(b), nil
}

func main() {
	persons := []Person{
		{Name: "name1", Surname: "surname1"},
		{Name: "name2", Surname: "surname2"},
		{Name: "name3", Surname: "surname3"},
		{Name: "name4", Surname: "surname4"},
	}

	f, err := os.Create("1.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	buf := bytes.NewBuffer(make([]byte, 0))

	w := io.MultiWriter(f, buf, os.Stdout)
	for i := range persons {
		w = io.MultiWriter(w, &persons[i])
	}

	enc := xml.NewEncoder(w)
	if err := enc.Encode(persons); err != nil {
		log.Fatal(err)
	}

	fmt.Println("Buffer string:", buf.String())
}

In this code, we first create a slice of Person structs, with each Person having a Write method that fulfills the io.Writer interface. We then create a file, a byte buffer, and a MultiWriter that writes to the file, the buffer, and standard output (os.Stdout).

We loop over the persons slice, creating a new MultiWriter for each Person that writes to all previous destinations plus the current Person.

We then create an XML encoder that writes to our final MultiWriter, and encode our persons slice to it. The encoding is written to all our destinations: the file, the buffer, standard output, and each Person. The final line of our program prints the contents of our buffer, which should contain the XML encoding.

1
2
3
4
5
<Person><Name>name1</Name><Surname>surname1</Surname></Person><Person><Name>name2</Name><Surname>surname2</Surname></Person><Person><Name>name3</Name><Surname>surname3</Surname></Person><Person><Name>name4</Name><Surname>surname4</Surname></Person>
Write method ran. Name of person is name1 
Write method ran. Name of person is name2 
Write method ran. Name of person is name3 
Write method ran. Name of person is name4

In the output, you’ll see that the XML encoding is written to the console, and also that the Write method is called on each Person object. This shows that io.MultiWriter indeed writes to all provided writers, making it a useful tool for writing the same data to multiple destinations.

Conclusion

The MultiWriter type in the Go’s io package is a powerful and versatile tool that offers the ability to write the same data to multiple locations in a straightforward manner. By reducing redundancy, optimizing resources, and enhancing logging and monitoring, MultiWriter can be a valuable addition to your Go programming toolbox.

Happy coding!

This post is licensed under CC BY 4.0 by the author.