io.Reader and io.Writer in Go
Go interfaces are one of the language features I enjoy using the most. They’re so simple yet they’re incredibly powerful when trying to write clear and maintainable code. io.Reader
and io.Writer
are arguably two of the most common in Go code (apart from the Error
interface) and they represent what’s so great about the humble interface.
If you want a quick primer on interfaces in Go, Go By Example is a great reference.
io.Reader
io.Reader
is a single method interface that represents a type from which bytes can be read from.
type Reader interface {
Read(p []byte) (n int, err error)
}
There are various examples of types in the Go standard library that implement the io.Reader
interface such as buffers of bytes, HTTP request and response bodies, and files.
The interface itself encapsulates a Read
method which takes a byte slice as an argument and returns an integer value representing the number of bytes read to the slice and an error value if one is encountered.
Read
will read all of the bytes available in the stream into the provided byte slice until it either runs out of bytes to read or fills the byte slice. When the reader does run out of bytes to read, it will return the number of bytes read and an io.EOF
error.
It is less common that you will directly use the Read
method in your code. Instead, you’re much more likely to use the helper functions provided by various packages that expect an io.Reader
and abstract the use of Read
from you. One such example is the io.ReadAll
function.
r := strings.NewReader("Hello, world!")
bytes, err := io.ReadAll(r)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", bytes)
Hello, world!
Click here to run this example on the Go Playground
strings.NewReader
creates astrings.Reader
which implementsio.Reader
.r
is passed toio.ReadAll
which takes anio.Reader
as an argument.bytes
is a byte slice containing all of the data stored in thestrings.Reader
.
io.Writer
io.Writer
is a single method interface that represents a type to which bytes can be written to.
type Writer interface {
Write(p []byte) (n int, err error)
}
Again, we can see plenty of examples of types that implement io.Writer
in Go’s standard library. HTTP request and response bodies, buffers of bytes, and files are just a few.
The io.Writer
interface encapsulates a single Write
method which takes a byte slice as an argument and returns an integer value representing the number of bytes written to the underlying type from the slice and an error value if one is encountered.
Write
will write all of the bytes passed in the byte slice to the underlying type that implements the interface. If the number of bytes written is less than the length of the slice then the Write
method will return a non-nil error to give the caller context as to why all of the bytes were not written. Something else to note here is that Write
will never modify the byte slice provided as an argument.
Like io.Reader
, you are much more likely to use helper functions provided by various packages that abstract the direct use of Write
. One very common example is the fmt.Fprintf
function.
s := "Hello world!"
fmt.Fprintf(os.Stdout, "%s, %d", s, len(s))
Hello world!, 12
Click here to run this example on the Go Playground
fmt.Fprintf
takes anio.Writer
as an argument and writes the formatted string.os.Stdout
is a file pointer that implementsio.Writer
.
io.Writer and io.Reader Composition
One of the reasons that io.Reader
and io.Writer
are so powerful is that they are concise and encapsulate a single behaviour. This means that it’s easy to include them when defining new interfaces or combine them with other interfaces to tightly encapsulate more than one behaviour.
As an example, io.ReadWriter
includes the behaviour of both the io.Reader
and io.Writer
interfaces.
type ReadWriter interface {
Reader
Writer
}
Another example is the io.ReadCloser
interface, which encapsulates the behaviour of an io.Reader
and io.Closer
. The io.Closer
interface is similar to io.Reader
and io.Writer
in that it wraps a single method Close
which allows the caller to close the underlying type, such as a file or a network connection.
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
io.ReadCloser
can be seen when working with net/http
request body types. The Body
field of the http.Request
struct implements io.ReadCloser
.
res, err := http.Get("https://http.cat/102")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
bytes, err := io.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
err = os.WriteFile("102.jpg", bytes, 0666)
if err != nil {
log.Fatal(err)
}
res.Body.Close()
is deferred to avoid a memory leak after the caller returns.res.Body
is passed directly toio.ReadAll
which accepts anio.Reader
as an argument.
The example shown above is correct as of Go 1.16. In this version,
io/ioutil
was deprecated and functionality provided by that package was moved intoio
andos
(see Go 1.16 release notes). To make the above example compatible with older versions of Go, replaceio.ReadAll
withioutil.ReadAll
andos.WriteFile
withioutil.WriteFile
.