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))
}
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
}
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.