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

challenge icon

Задание

Легко

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

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

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