io.Reader и io.Writer
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 73 из 107.
Стандартная библиотека Go демонстрирует дизайн на основе интерфейсов через два фундаментальных интерфейса: io.Reader и io.Writer. Эти простые интерфейсы обеспечивают работу всего: от файловых операций до сетевого взаимодействия.
Интерфейс io.Reader требует всего один метод:
type Reader interface {
Read(p []byte) (n int, err error)
}Он считывает до len(p) байтов в слайс и возвращает количество прочитанных байтов. Аналогично, io.Writer определяет:
type Writer interface {
Write(p []byte) (n int, err error)
}Любой тип, реализующий эти методы, работает со всей экосистемой ввода-вывода.
Вот пользовательский тип, который реализует io.Reader:
type RepeatReader struct {
Char byte
Count int
pos int
}
func (r *RepeatReader) Read(p []byte) (int, error) {
if r.pos >= r.Count {
return 0, io.EOF
}
n := 0
for n < len(p) && r.pos < r.Count {
p[n] = r.Char
n++
r.pos++
}
return n, nil
}Теперь этот пользовательский ридер работает с любой функцией, ожидающей io.Reader:
reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data)) // AAAAAЭтот дизайн позволяет вам писать функции, которые принимают io.Reader или io.Writer, что позволяет им работать с файлами, сетевыми соединениями, буферами или любой пользовательской реализацией.
Задание
ЛегкоДавайте создадим собственный процессор текстовых потоков, который реализует интерфейсы io.Reader и io.Writer! Вы создадите типы, которые могут преобразовывать текст по мере его прохождения через них, демонстрируя, как эти фундаментальные интерфейсы обеспечивают мощную композицию ввода-вывода (I/O).
Вы организуете свой код в двух файлах:
streams.go: Определите свои пользовательские типы ридера и райтера.Создайте структуру
CountingReader, которая оборачивает строку и отслеживает, сколько байт было прочитано. Она должна иметь поля для исходной строки и трекер позиции. Реализуйте методRead(p []byte) (int, error), который считывает байты из источника в предоставленный слайс, обновляет позицию и возвращаетio.EOF, когда источник исчерпан.Создайте структуру
UppercaseWriter, которая собирает записанные данные и преобразует их в верхний регистр. Она должна хранить накопленный результат внутри. Реализуйте методWrite(p []byte) (int, error), который преобразует входящие байты в верхний регистр и сохраняет их. Добавьте методResult() stringдля получения накопленного текста в верхнем регистре.Создайте функции-конструкторы
NewCountingReader(source string) *CountingReaderиNewUppercaseWriter() *UppercaseWriterдля правильной инициализации ваших типов.main.go: Продемонстрируйте работу ваших пользовательских типов ввода-вывода со стандартной библиотекой.Прочитайте строковый ввод, затем используйте ваш
CountingReaderвместе сio.ReadAllдля чтения всех данных. Пропустите эти данные через вашUppercaseWriter, используя его методWrite.Выведите результаты в следующем формате:
Original: [input] Uppercase: [result from writer] Bytes read: [total bytes]
Будут предоставлены следующие входные данные:
- Одна строка текста
Например, при вводе:
Hello WorldВаш вывод должен быть:
Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11И при вводе:
Go interfaces are powerfulВаш вывод должен быть:
Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26Ключевая идея заключается в том, что ваш CountingReader беспрепятственно работает с io.ReadAll, потому что он реализует io.Reader. Любая функция в экосистеме Go, принимающая io.Reader, будет работать с вашим пользовательским типом — в этом и заключается мощь дизайна на основе интерфейсов.
Шпаргалка
Интерфейсы Go io.Reader и io.Writer являются фундаментальными для операций ввода-вывода:
type Reader interface {
Read(p []byte) (n int, err error)
}type Writer interface {
Write(p []byte) (n int, err error)
}Метод Read считывает до len(p) байт в срез и возвращает количество прочитанных байт. Метод Write записывает байты из среза и возвращает количество записанных байт.
Любой тип, реализующий эти методы, работает со всей экосистемой ввода-вывода Go. Пример пользовательского ридера:
type RepeatReader struct {
Char byte
Count int
pos int
}
func (r *RepeatReader) Read(p []byte) (int, error) {
if r.pos >= r.Count {
return 0, io.EOF
}
n := 0
for n < len(p) && r.pos < r.Count {
p[n] = r.Char
n++
r.pos++
}
return n, nil
}Использование пользовательского ридера со стандартными функциями библиотеки:
reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data)) // AAAAAТакой дизайн позволяет функциям, принимающим io.Reader или io.Writer, работать с файлами, сетевыми соединениями, буферами или любой пользовательской реализацией.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// Чтение входных данных
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
// Удаление завершающего символа новой строки, если он есть
if len(input) > 0 && input[len(input)-1] == '\n' {
input = input[:len(input)-1]
}
// TODO: Создайте CountingReader со строкой ввода
// countingReader := NewCountingReader(input)
// TODO: Используйте io.ReadAll для чтения всех данных из вашего CountingReader
// data, err := io.ReadAll(countingReader)
// TODO: Создайте UppercaseWriter
// upperWriter := NewUppercaseWriter()
// TODO: Запишите данные в ваш UppercaseWriter
// upperWriter.Write(data)
// TODO: Выведите результаты в требуемом формате:
// Original: [input]
// Uppercase: [result from writer]
// Bytes read: [total bytes]
// Заглушка для предотвращения ошибки неиспользуемого импорта - удалите при реализации
_ = io.EOF
fmt.Println("Original:", input)
// fmt.Println("Uppercase:", upperWriter.Result())
// fmt.Println("Bytes read:", countingReader.Position)
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool