41 Interfaces and Tools in the io Package - Part 2 #
In the previous article, I mainly focused on the extension interfaces and implementation types of io.Reader
. However, the io
package contains more than just the io.Reader
interface.
What I covered earlier was only a part of the io
package’s type hierarchy. It is necessary to explore it from another perspective in order to have a more comprehensive understanding of the package.
The following question is related to this.
Knowledge Expansion #
Question: What are the interfaces in the io
package? What are their relationships?
#
We can call an interface that is not embedded in other interfaces and only defines one method a simple interface. In the io
package, there are a total of 11 such interfaces.
Among them, some interfaces have many extension interfaces and implementation types, which we can call core interfaces. The core interfaces in the io
package are only 3: io.Reader
, io.Writer
, and io.Closer
.
We can also divide the simple interfaces in the io
package into four categories. These four categories of interfaces correspond to four types of operations: reading, writing, closing, and setting read/write positions. The first three operations belong to basic I/O operations.
Regarding the read operation, we have already discussed the core interface io.Reader
in detail earlier. It has 5 extension interfaces and 6 implementation types in the io
package. Besides that, there are also other interfaces in this package specifically for reading operations. Let’s organize them below.
Let’s start with the io.ByteReader
and io.RuneReader
interfaces. They each define a single read method: ReadByte
and ReadRune
respectively.
But unlike the Read
method in the io.Reader
interface, these two read methods can only read the next individual byte or Unicode character.
The data types strings.Reader
and bytes.Buffer
that we have mentioned before are both implementation types of io.ByteReader
and io.RuneReader
.
Moreover, both of these types also implement the io.ByteScanner
interface and the io.RuneScanner
interface.
The io.ByteScanner
interface embeds the simple interface io.ByteReader
and defines an additional method called UnreadByte
. This abstracts the functionality of reading and unreading a single byte.
Similarly, the io.RuneScanner
embeds the simple interface io.RuneReader
and defines an additional method called UnreadRune
. It abstracts the functionality of reading and unreading a single Unicode character.
Next, let’s look at the io.ReaderAt
interface. It is also a simple interface that only defines one method: ReadAt
. Unlike the read methods we mentioned earlier, ReadAt
is a pure read method.
It only reads the bytes contained in its value and does not make any modifications to the value itself, such as modifying the read count. This is the most important convention between the io.ReaderAt
interface and its implementation types.
Therefore, if you only call the ReadAt
method of a value concurrently, its safety should be guaranteed.
In addition, there is another interface related to read operations that we haven’t introduced, which is the io.WriterTo
interface. This interface defines a method called WriteTo
.
Don’t be confused by its name, this WriteTo
method is actually a read method. It accepts a value of type io.Writer
as a parameter and reads the data in its own value and writes it to that parameter value.
Correspondingly, the io.ReaderFrom
interface defines a write method called ReadFrom
. This method accepts a value of type io.Reader
as a parameter, reads data from that parameter value, and writes it into its own value.
It is worth mentioning that the io.CopyN
function we used before checks whether the value of its src
parameter implements the io.WriterTo
interface before copying the data. If it does, then it directly uses the WriteTo
method of that value to copy the data to the value represented by the dst
parameter.
Likewise, this function also checks whether the value of dst
implements the io.ReaderFrom
interface. If it does, then it uses the ReadFrom
method of that value to directly copy the data from src
to it.
In fact, the same applies to the io.Copy
function and the io.CopyBuffer
function because they use the same set of code to do data copying internally.
As you can see, the io.ReaderFrom
interface corresponds well with the io.WriterTo
interface. In fact, in the io
package, interfaces related to write operations have a certain correspondence with the corresponding read operation interfaces. Now, let’s talk about the interfaces related to write operations.
First, of course, there is the core interface io.Writer
. In addition to the known extensions of this interface such as io.ReadWriter
, io.ReadWriteCloser
, and io.ReadWriteSeeker
, there are also io.WriteCloser
and io.WriteSeeker
.
The *io.pipe
that we mentioned earlier is an implementation type of the io.ReadWriter
interface. However, there are no implementations of the io.ReadWriteCloser
interface in the io
package, and its implementation types are mainly concentrated in the net
package.
In addition to that, there are two simple interfaces related to write operations: io.ByteWriter
and io.WriterAt
. Unfortunately, there are no implementation types of them in the io
package. However, there is one type worth mentioning here, which is *os.File
.
This type is not only an implementation type of the io.WriterAt
interface but also implements the io.ReadWriteCloser
and io.ReadWriteSeeker
interfaces at the same time. In other words, this type supports a wide range of I/O operations.
The io.Seeker
interface, as a simple interface related to setting the read/write position, also only defines one method called Seek
.
When I talked about the Seek
method of the strings.Reader
type, I specifically mentioned this method earlier and provided an example related to the estimated read count. This method is mainly used to find and set the starting index position for the next read or write.
The io
package has several extension interfaces based on io.Seeker
, including io.ReadSeeker
and io.ReadWriteSeeker
mentioned earlier, as well as io.WriteSeeker
which was not mentioned before. io.WriteSeeker
is an extension interface based on io.Writer
and io.Seeker
.
The two pointer types strings.Reader
and io.SectionReader
that we mentioned many times also implement the io.Seeker
interface. By the way, these two types are also implementation types of the io.ReaderAt
interface.
Finally, the io.Closer
interface, which is related to closing operations, is very versatile, with many extension interfaces and implementation types. Just by their names, we can see at a glance which interfaces in the io
package are its extensions. As for its implementation types, the io
package only has io.PipeReader
and io.PipeWriter
.
Summary #
Let’s summarize the content of these two articles. In Go language, interface extension is achieved through interface type embedding, which is often referred to as interface composition. The io
package is a benchmark for interface extension and can serve as a reference standard for using this technique.
In this article, I classified the interfaces in the io
package into simple interfaces and extended interfaces based on the number of methods defined by the interfaces and whether there is interface embedding.
Furthermore, I divided these simple interfaces into core interfaces and non-core interfaces based on the number of extended interfaces and implementation types.
In the io
package, there are only three simple interfaces that can be considered as core interfaces: io.Reader
, io.Writer
, and io.Closer
. These core interfaces have more than 200 implementation types in the Go standard library.
Additionally, I categorized the simple interfaces into four major types based on the different I/O operations they target: reading, writing, closing, and seek setting.
The first three types of operations belong to basic I/O operations. Based on this, I organized each category of simple interfaces and explained their corresponding extended interfaces and representative implementation types in the io
package.
Apart from that, I also described the functions and mechanisms of several important program entities from multiple perspectives, such as the data segment reader io.SectionReader
, the io.pipe
type used as the core implementation for synchronous memory channels, and the io.CopyN
function used for data copying.
I provided such detailed and multi-angle explanations in order to help you memorize the interrelated interfaces and data types in the io
package. I hope this purpose has been achieved, and at the very least, this article can serve as a starting point for your profound understanding of them.
Lastly, I want to emphasize that there are a total of 11 simple interfaces in the io
package. Among them, there are 5 interfaces related to reading operations, 4 interfaces related to writing operations, 1 interface related to closing operations, and 1 interface related to seek setting. Additionally, the io
package also contains 9 extension interfaces based on these simple interfaces. In the future, you will need to consider and practice when to write which data type to implement which interfaces in the io
package in order to maximize their benefits.
Thought question #
Today’s thought question is: What is the operating mechanism of the synchronous memory channel in the io
package?
Click here to view the detailed code for the Go language column.