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) bytes в срез и возвращает, how many bytes было прочитано. Аналогично, io.Writer определяет:
type Writer interface {
Write(p []byte) (n int, err error)
}Любой тип, implementing эти методы, работает со всей экосистемой I/O.
Вот пользовательский тип, который реализует 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! Вы создадите типы, способные преобразовывать текст по мере его прохождения через них, демонстрируя, как эти фундаментальные interfaces обеспечивают powerful композицию операций ввода-вывода.
Вы организуете код в двух файлах:
streams.go: Определите пользовательские типы reader и writer.Создайте структуру
CountingReader, которая оборачивает строку и отслеживает, сколько bytes было прочитано. Она должна иметь поля для исходной строки и отслеживания position. Реализуйте методRead(p []byte) (int, error), который считывает bytes из источника в предоставленный срез, обновляет position и возвращаетio.EOF, когда источник исчерпан.Создайте структуру
UppercaseWriter, которая собирает записанные data и преобразует их в верхний регистр. Она должна внутренне хранить accumulated результат. Реализуйте методWrite(p []byte) (int, error), который преобразует incoming bytes в верхний регистр и сохраняет их. Добавьте методResult() stringдля получения accumulated текста в верхнем регистре.Создайте функции-конструкторы
NewCountingReader(source string) *CountingReaderиNewUppercaseWriter() *UppercaseWriter, чтобы правильно Initialize ваши типы.main.go: Продемонстрируйте работу ваших пользовательских типов ввода-вывода со стандартной библиотекой.Считайте строку input, затем используйте ваш
CountingReaderвместе сio.ReadAll, чтобы считать все data. Передайте эти data через вашUppercaseWriter, используя его методWrite.Выведите результаты в следующем формате:
Original: [input] Uppercase: [result from writer] Bytes read: [total bytes]
Будет предоставлен следующий input:
- Одна строка текста
Например, если given:
Hello WorldВаш вывод должен быть таким:
Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11А если given:
Go interfaces are powerfulВаш вывод должен быть таким:
Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26Ключевая идея заключается в том, что ваш CountingReader без проблем работает с io.ReadAll, поскольку он реализует io.Reader. Любая function в экосистеме Go, принимающая io.Reader, будет работать с вашим пользовательским типом: в этом и заключается сила проектирования на основе interfaces.
Попробуйте сами
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: [результат из writer]
// Bytes read: [всего байт]
// Заглушка, чтобы избежать ошибки неиспользуемого импорта — удалить при реализации
_ = 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Потренируйтесь самостоятельно: Онлайн-компилятор Go