41 Interfaces and Tools in the Io Package Part 2

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.

io interfaces hierarchy

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.