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