fmt.Stringer

type Stringer interface {
    String() string
} 

Этот интерфейс часто используется, когда нужно одной строчкой залогировать сложный объект. Определение интерфейса лежит в пакете fmt.

Для примера возьмём структуру User и допишем к ней реализацию интерфейса fmt.Stringer:

type User struct {
    Email        string
    PasswordHash string
    LastAccess   time.Time
}
 
func (u User) String() string {
    return "user with email " + u.Email
}
 
func main() {
    u := User{Email: "example@yandex.ru"}
    fmt.Printf("Hello, %s", u)
} 

Код выведет:

Hello, user with email example@yandex.ru 

Функция fmt.Printf использовала реализацию интерфейса.

Пакет io

Пакет io предназначен для реализации средств ввода-вывода, однако в нём есть несколько удобных интерфейсов, которые применяются и для других целей.

io.Reader

type Reader interface {
    Read(p []byte) (n int, err error)
} 

Этот интерфейс описывает чтение из любого потока данных: сети, файловой системы или буфера. Определение интерфейса лежит в пакете io.

Метод Read считывает в переданный слайс байт данные из источника. В качестве источника могут выступать любые данные, которые описаны в типе. То есть считываем их структуры и записываем в байты. Количество считанных байт неявно задаётся размером буфера — длиной слайса.

Объясним возможности интерфейса на примере. Есть буфер — и нужно прочитать байты из него. В пакете strings лежит функция strings.NewReader, которая оборачивает обычную строку в структуру strings.Reader. Эта структура имеет метод Read, значит, она реализует интерфейс io.Reader:

s := `Hodor. Hodor hodor, hodor. Hodor hodor hodor hodor hodor. Hodor. Hodor! 
Hodor hodor, hodor; hodor hodor hodor. Hodor. Hodor hodor; hodor hodor - hodor, 
hodor, hodor hodor. Hodor, hodor. Hodor. Hodor, hodor hodor hodor; hodor hodor; 
hodor hodor hodor! Hodor hodor HODOR! Hodor hodor... Hodor hodor hodor...`
 
// обернём строчку в strings.Reader
r := strings.NewReader(s)
 
// создадим буфер на 16 байт
b := make([]byte, 16)
 
for {
    // strings.Reader скопирует 16 байт в b
    //
    // в структуре запоминается последний указатель,  
    // то есть следующий вызов скопирует следующую порцию из 16 байт
    //
    // также метод возвращает количество прочитанных байт n и ошибку err
    //
    // когда дойдём до конца строки, метод отдаст ошибку io.EOF
    n, err := r.Read(b)
 
    // при работе с интерфейсом io.Reader нужно в первую очередь проверять
    // n > 0, затем err != nil
    //
    // могут быть ситуации, когда часть данных получилось прочитать
    // и сохранить в буфер, а затем произошла ошибка 
    //
    // в таком случае будут одновременно n > 0 и err != nil
    if n > 0 {
        // выведем на экран содержимое буфера
        fmt.Printf("%v\n", b)
    }
 
    if err != nil {
        // если дочитали до конца, выходим из цикла
        if errors.Is(err, io.EOF) {
            break
        }
 
        // обрабатываем ошибку чтения
        fmt.Printf("error: %v\n", err)
    }
} 

Удобство применения io.Reader в том, что его пользователь может вообще не знать, откуда берутся данные: из файла, сети или генерируются на лету. Интерфейс описывает унифицированный метод работы с ними.

io.Writer

type Writer interface {
    Write(p []byte) (n int, err error)
} 

Этот интерфейс означает запись в любой возможный поток данных: сетевой сокет, файл или буфер. Определение интерфейса лежит в пакете io.

C этим интерфейсом ситуация, обратная io.Reader. Он позволяет записать переданный ему слайс байт куда-то. Куда именно — определяется реализацией.

Для примера соберём большую строку из подстрок, вот только не через оператор +=, потому что тогда на каждую итерацию будет лишняя копия всей строки. В пакете strings есть структура strings.Builder для сборки строки без избыточного копирования. Эта структура имеет метод Write, значит, она реализует интерфейс io.Writer:

package main
 
import (
	"fmt"
	"strings"
)
 
func main() {
	w := strings.Builder{}
 
	for i := 0; i < 50; i++ {
		// функция fmt.Fprintf принимает аргументом io.Writer
		// благодаря этому можно записывать форматированный вывод
		fmt.Fprintf(&w, "%d ", i)
	}
 
	w.Write([]byte("... Some text"))
 
	// выводим собранную строку
	// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 ... Some text
	fmt.Printf("%s\n", &w)
}

Приведём пример реализации интерфейса Write. Предположим, что мы хотим посчитать хеш от некоторого массива байт или наборов массивов. Для простоты возьмём упрощённую функцию хеширования:

package hashbyte
 
import "io"
 
type Hasher interface {
    io.Writer // мы встроили интерфейс io.Writer в наш интерфейс, чтобы задать требование по наличию метода Write
    Hash() byte
}
 
type hash struct {
    result byte
}
 
func New(_init byte) Hasher {
    return &hash{
        result: _init,
    }
}
 
// Write — сюда может быть записан массив байт любой длины, для которой будет подсчитываться хэш.
func (h *hash) Write(bytes []byte) (n int, err error) {
    // обновляем хеш для каждого байта, записанного в хешер
    for _, b := range bytes {
        h.result = (h.result^b)<<1 + b%2 
    }
    return len(bytes), nil
}
 
func (h hash) Hash() byte {
    return h.result
} 

Функции-утилиты для io.Reader и io.Writer

io.Copy

func Copy(dst Writer, src Reader) (written int64, err error) 

Функция копирует все байты из io.Reader в io.Writer.

Данные будут считываться до тех пор, пока функция Read не вернёт вторым аргументом ошибку. Если в качестве ошибки будет возвращено значение io.EOF, то выполнение функции закончится без ошибок. Также будет возвращено количество байт.

io.EOF происходит от end of frame (конец файла) — исторически так назывался специальный символ, который означал конец файла.

Приведём простой пример. Напишем функцию, копирующую содержимое одного файла в другой:

func CopyFile(srcFileName, dstFileName string) error {
    srcFile, err := os.Open(srcFileName)
    if err != nil {
        return err
    }
    dstFile, err := os.Create(dstFileName)
    if err != nil {
        return err
    }
    n, err := io.Copy(dstFile, srcFile)
    if err != nil {
        return err
    }
    fmt.Printf("Copied %d bytes from %s to %s", n, srcFileName, dstFileName)
    return nil
}
 

Структура типа os.File реализует интерфейсы io.Reader и io.Writer.

Было бы просто считать весь исходный файл в память и затем скопировать его в новый. Но если исходный файл занимает сотни гигабайт? io.Copy работает умнее, считывая и записывая данные небольшими кусочками, поэтому для подобных операций рекомендуется использовать именно её.

io.CopyN

func CopyN(dst Writer, src Reader, n int64) (written int64, err error) 

Функция копирует все байты из io.Reader в io.Writer, но не более n байт. То же самое, что и Copy, но с ограничением — можно использовать с источниками данных, которые слишком большие или вообще бесконечные. Например, напишем функцию, которая будет сохранять данные из нашего генератора случайных чисел в файл.

// Dump — сохраняет вычисленные данные в файл
func (g generator) Dump(n int64, dst *os.File) error {
    _, err := io.CopyN(g, dst, n)
    return err
} 

Если бы мы использовали Copy, то программа продолжила бы работать до переполнения диска.

io.ReadAll

func ReadAll(r Reader) ([]byte, error) 

Функция считывает все байты из io.Reader. Чтение закончится, когда io.Reader вернёт io.EOF.

io.ReadAtLeast

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) 

Функция считывает байты из io.Reader c ограничением: если прочитанных байт оказалось меньше, чем n, вернётся ошибка io.ErrUnexpectedEOF. Это используется при парсинге бинарных данных, чтобы гарантировать, что нужное минимальное количество байт будет вычитано.


📂 Go | Последнее изменение: 28.08.2024 13:25