Язык Go имеет свою модель работы с потоками ввода-вывода, которая позволяет получать данные из различных источников - файлов, сетевых интерфейсов, объектов в памяти и т.д.
Поток данных в Go представлен байтовым срезом ([]byte), из которого можно считывать байты или в который можно заносить данные. Ключевыми типами для работы с потоками являются интерфейсы Reader и Writer из пакета io.
Интерфейс io.Reader предназначен для считывания данных. Он имеет следующее определение:
type Reader interface { Read(p []byte) (n int, err error) }
Метод Read возвращает общее количество считанных байт из среза байт и информацию об ошибке, если она возникнет. Если в потоке больше нет данных, то метод должен возвращать ошибку типа io.EOF.
Рассмотрим простейший пример. Например, нам необходимо считывать номера телефонов, которые могут иметь различные форматы:
package main import ( "fmt" "io" ) type phoneReader string func (ph phoneReader) Read(p []byte) (int, error){ count := 0 for i := 0; i < len(ph); i++{ if(ph[i] >= '0' && ph[i] <= '9'){ p[count] = ph[i] count++ } } return count, io.EOF } func main() { phone1 := phoneReader("+1(234)567 9010") phone2 := phoneReader("+2-345-678-12-35") buffer := make([]byte, len(phone1)) phone1.Read(buffer) fmt.Println(string(buffer)) // 12345679010 buffer = make([]byte, len(phone2)) phone2.Read(buffer) fmt.Println(string(buffer)) // 23456781235 }
Для считывания номеров телефонов определен тип phoneReader, который по сути представляет тип string. Однако phoneReader при этом реализует интерфейс Reader, то есть определяет его метод Read. В методе Read считываем данные из строки, которую представляет объект phoneReader и, если символы строки представляют числовые данные, передаем их в срез байтов. На выходе возвращаем количество считанных данных и маркер окончания чтения io.EOF. В результате при считывании из строки метод Read возвратит номер телефона, который состоит только из цифр.
При вызове метода Read создается срез байтов достаточной длины, который передается в метод Read:
buffer := make([]byte, len(phone1)) phone1.Read(buffer)
Затем с помощью инициализатора string мы можем преобразовать срез байтов в строку:
fmt.Println(string(buffer)) // 12345679010
Интерфейс io.Writer предназначен для записи в поток. Он определяет метод Write():
type Writer interface { Write(p []byte) (n int, err error) }
Метод Write предназначен для копирования данных их среза байт p в определенный ресурс - файл, сетевой интерфейс и т.д. Метод возвращает количество записанных байтов и объект ошибки.
Рассмотрим примитивный пример:
package main import "fmt" type phoneWriter struct{ } func (p phoneWriter) Write(bs []byte) (int, error){ if len(bs) == 0 { return 0, nil } for i := 0; i < len(bs); i++{ if(bs[i] >= '0' && bs[i] <= '9'){ fmt.Print(string(bs[i])) } } fmt.Println() return len(bs), nil } func main() { bytes1 := []byte("+1(234)567 9010") bytes2 := []byte("+2-345-678-12-35") writer := phoneWriter{} writer.Write(bytes1) writer.Write(bytes2) }
Здесь структура phoneWriter реализует интерфейс Writer. В методе Write она принимает срез байтов. Предполагается, что срез байтов хранит номер телефона. Эта информация должным образом обрабатывается: из нее выделяются цифры, которые выводятся на консоль. То есть тип phoneWriter осуществляет запись потока байт на консоль.
В качестве результата метод возвращает длину среза и значение nil.
Для имитации потока байт определяются два среза байт на основе строк, которые передаются в метод Write.
На основе выше рассмотренных интерфейсов Writer и Reader основана вся система ввода-вывода в Go, и впоследствии мы более детально рассмотрим их примение при работе с файлами и сетевыми потоками.