Skip to main content

Our first Writer

In Our First Reader chapter, I introduced you to io.Reader interface using the strings.Reader implementation.

You might assume that now I will introduce you to a strings.Writer, but the truth is that there is no such type.

If you think about it, strings are just bytes, but they are immutable, meaning that you cannot change them. So I guess, that's the reason there is no such thing as strings.Writer, because you cannot write to a string.

So let's find the closest type to a bunch of bytes which can be written to: bytes.Buffer.

bytes.Buffer

A Buffer is a variable-sized buffer of bytes with Read and Write methods. The zero value for Buffer is an empty buffer ready to use, say the docs.

So the bytes.Buffer is both a Reader and a Writer. This is good news: because we can write into it to showcase the io.Writer behaviour and then we will read from it to see that we actually wrote into it.

package main

import (
"bytes"
"fmt"
"io"
"log"
)

func main() {
var buf bytes.Buffer

n, err := buf.Write([]byte("Hello Writer!"))
if err != nil {
log.Fatal(err)
}

fmt.Println("Bytes written", n)

whatWasWritten, err := io.ReadAll(&buf)
if err != nil {
log.Fatal(err)
}

fmt.Println("What was written into buf:", string(whatWasWritten))

}

run in Playground

Output:

Bytes written 13
What was written into buf: Hello Writer!

Program exited.

You'll notice that we had to use the address of buf &buf instead of buf:

whatWasWritten, err := io.ReadAll(&buf)

That's because the Read method has a pointer receiver, in other words Read belongs to *Buffer not Buffer:

// Read reads the next len(p) bytes from the buffer or until the buffer
// is drained. The return value n is the number of bytes read. If the
// buffer has no data to return, err is io.EOF (unless len(p) is zero);
// otherwise it is nil.
func (b *Buffer) Read(p []byte) (n int, err error) {
b.lastRead = opInvalid
if b.empty() {
// Buffer is empty, reset to recover space.
b.Reset()
if len(p) == 0 {
return 0, nil
}
return 0, io.EOF
}
n = copy(p, b.buf[b.off:])
b.off += n
if n > 0 {
b.lastRead = opRead
}
return n, nil
}

source

The same story goes for the Write() method. If you need to pass a bytes.Buffer as an argument to a function then you need to pass its address.

fmt.Fprintf

fmt.Fprintf is another helpful function to write a string to an io.Writer because it accepts an w io.Writer argument:

func Fprintf(w io.Writer, format string, a ...any) (n int, err error)

We can improve a bit our Hello Writer example from above:

package main

import (
"bytes"
"fmt"
"io"
"log"
)

func main() {
var buf bytes.Buffer

fmt.Fprintf(&buf, "Hello Writer!")

whatWasWritten, err := io.ReadAll(&buf)
if err != nil {
log.Fatal(err)
}

fmt.Println("What was written into buf:", string(whatWasWritten))
}

io.WriteString

You can also use the io.WriteString function to write to an io.Writer:

package main

import (
"bytes"
"fmt"
"io"
"log"
)

func main() {
var buf bytes.Buffer

io.WriteString(&buf, "Hello Writer!")

whatWasWritten, err := io.ReadAll(&buf)
if err != nil {
log.Fatal(err)
}

fmt.Println("What was written into buf:", string(whatWasWritten))

}

Conclusion

This ends our first introduction into the io.Writer. We got to meet bytes.Buffer - a type which is both a Writer and a Reader, and we wrote some strings into it with the help of fmt.Fprintf and io.WriteString.

Next it's time to see other concrete examples of Reader and Writers.