Menu
Coddy logo textTech

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, благодаря чему они работают с файлами, сетевыми соединениями, буферами или любой пользовательской реализацией.

challenge icon

Задание

Легко

Давайте создадим пользовательский процессор текстового потока, реализующий интерфейсы 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)
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Go