Read and Write in Golang

The Go language has its model of working with I/O streams, which allows you to receive data from various sources files, network interfaces, objects in memory, etc.

A data stream in Go is represented by a byte slice ([]byte) from which bytes can be read or into which data can be written. The key types for working with streams are the Reader and Writer interfaces from the io package.

The io.Reader interface is designed to read data. It has the following definition:

type Reader interface { 
    Read(p []byte) (n int, err error) 
}

The Read method returns the total number of bytes read from the byte slice and error information if any occurred. If there is no more data in the stream, then the method should return an error of type io.EOF.

Let's look at a simple example. For example, we need to read phone numbers, which can have different formats:

package main

import (
	"fmt"
	"io"
)

type letterReader string

func (lr letterReader) Read(p []byte) (int, error) {
	count := 0
	for i := 0; i < len(lr); i++ {
		if (lr[i] >= 'a' && lr[i] <= 'z') || (lr[i] >= 'A' && lr[i] <= 'Z') {
			p[count] = lr[i]
			count++
		}
	}
	return count, io.EOF
}

func main() {
	text1 := letterReader("Hello World")
	text2 := letterReader("Read this letter")

	buffer := make([]byte, len(text1))
	text1.Read(buffer)
	fmt.Println(string(buffer)) // Hello World

	buffer = make([]byte, len(text2))
	text2.Read(buffer)
	fmt.Println(string(buffer)) // Read this letter
}

To extract letters from a given string, the letterReader type is defined, which essentially represents the string type. The letterReader type implements the Reader interface, defining its Read method. Within the Read method, we iterate through the characters in the string represented by the letterReader object.
If the characters in the string are alphabetic, we pass them to a byte slice. The method returns the amount of data read and the end-of-reading marker io.EOF. Consequently, when reading from a string, the Read method will return a string containing only letters.

When the Read method is called, a byte slice of sufficient length is created and passed to the Read method:

buffer := make([]byte, len(text2))
text2.Read(buffer)

Then, using the string initializer, we can convert the byte slice to a string:

fmt.Println(string(buffer))

Writer Interface

The io.Writer interface is designed for writing to a stream. It defines the Write() method:

type Writer interface { 
    Write(s []byte) (n int, err error) 
}

The Write method is intended to copy data from its byte slices to a specific resource a file, network interface, etc. 
The method returns the number of bytes written and an error object.

package main

import "fmt"

type letterWriter struct{}

func (lw letterWriter) Write(bs []byte) (int, error) {
	if len(bs) == 0 {
		return 0, nil
	}
	for i := 0; i < len(bs); i++ {
		if (bs[i] >= 'a' && bs[i] <= 'z') || (bs[i] >= 'A' && bs[i] <= 'Z') {
			fmt.Print(string(bs[i]))
		}
	}
	fmt.Println()
	return len(bs), nil
}

func main() {
	bytes1 := []byte("Hello World")
	bytes2 := []byte("Read this letter")

	writer := letterWriter{}
	writer.Write(bytes1)
	writer.Write(bytes2)
}

In this example, the letterWriter structure implements the Writer interface. The Write method  letterWriter accepts a slice of bytes, which is intended to represent a stream of letters. The information in the byte slice is processed accordingly: only letters are extracted, and they are then output to the console. Essentially, the letterWriter type writes a stream of letters to the console.

As a result, the Write method returns the length of the byte slice and the value nil.

To simulate a stream of bytes, two-byte slices are defined based on strings, which are then passed to the Write method.

It's worth noting that the entire I/O system in Go is built on the Writer and Reader interfaces discussed here. Later, we will delve into their usage in more detail, particularly when working with files and network streams.