Модуль
1Интерфейсы: основы2Стандартные интерфейсы: io.Reader, Stringer, error← вы здесь3Пустой интерфейс и any4Type assertion и type switch
Урок 2~0 минут

Стандартные интерфейсы: io.Reader, Stringer, error

Что такое стандартные интерфейсы?

В Go есть несколько интерфейсов, которые встречаются повсюду в стандартной библиотеке. Их называют "стандартными" или "встроенными" интерфейсами. Сегодня разберем три самых важных: io.Reader, Stringer и error.

Знание этих интерфейсов поможет вам писать код, который хорошо интегрируется с остальной экосистемой Go.


io.Reader — интерфейс для чтения данных

io.Reader — это, пожалуй, самый распространенный интерфейс в Go. Он используется везде, где нужно читать данные: из файлов, сетевых соединений, строк в памяти и т.д.

go
package main
 
import (
    "fmt"
    "io"
    "strings"
)
 
// Reader имеет всего один метод
// type Reader interface {
//     Read(p []byte) (n int, err error)
// }
 
func main() {
    // Создаем источник данных — строку
    data := "Hello, Reader interface!"
    
    // strings.Reader реализует io.Reader
    reader := strings.NewReader(data)
    
    // Буфер для чтения
    buffer := make([]byte, 8)
    
    // Читаем данные порциями
    for {
        n, err := reader.Read(buffer)
        if err == io.EOF {
            fmt.Println("\nДостигнут конец данных")
            break
        }
        if err != nil {
            fmt.Println("Ошибка чтения:", err)
            break
        }
        
        fmt.Printf("Прочитано %d байт: %s\n", n, buffer[:n])
    }
}

Ключевой момент: Read заполняет переданный срез байтами и возвращает количество прочитанных байт. Когда данные закончились, метод возвращает ошибку io.EOF.


Stringer — для красивого вывода

Интерфейс Stringer используется для преобразования значений в строки. Когда вы вызываете fmt.Println(), fmt.Printf() или другие функции форматирования, они автоматически ищут метод String().

go
package main
 
import "fmt"
 
type Person struct {
    Name string
    Age  int
}
 
// Реализуем интерфейс Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d лет)", p.Name, p.Age)
}
 
func main() {
    alice := Person{"Алиса", 25}
    bob := Person{"Боб", 30}
    
    // fmt.Println автоматически вызовет метод String()
    fmt.Println(alice) // Алиса (25 лет)
    fmt.Println(bob)   // Боб (30 лет)
    
    // Без реализации Stringer вывелось бы: {Алиса 25}
}

Этот интерфейс особенно полезен для отладки и логирования — вы можете контролировать, как ваши структуры отображаются в логах.


error — интерфейс для ошибок

В Go ошибки — это значения, которые реализуют интерфейс error. Это один из немногих интерфейсов, который имеет глобальное значение в языке.

go
package main
 
import (
    "errors"
    "fmt"
)
 
// Интерфейс error очень простой:
// type error interface {
//     Error() string
// }
 
// Создаем свою ошибку
type ValidationError struct {
    Field   string
    Message string
}
 
// Реализуем интерфейс error
func (e ValidationError) Error() string {
    return fmt.Sprintf("ошибка валидации поля %s: %s", e.Field, e.Message)
}
 
func validateUser(name string, age int) error {
    if name == "" {
        return ValidationError{"name", "имя не может быть пустым"}
    }
    if age < 0 {
        return ValidationError{"age", "возраст не может быть отрицательным"}
    }
    return nil
}
 
func main() {
    // Используем стандартную ошибку
    err := errors.New("что-то пошло не так")
    fmt.Println(err)
    
    // Используем нашу кастомную ошибку
    if err := validateUser("", -5); err != nil {
        fmt.Println(err)
        // Можно проверить тип ошибки
        if valErr, ok := err.(ValidationError); ok {
            fmt.Printf("Поле с ошибкой: %s\n", valErr.Field)
        }
    }
}

Создавать свои типы ошибок полезно, когда нужно передавать дополнительную информацию об ошибке или обрабатывать разные типы ошибок по-разному.


Практический пример: все вместе

Давайте создадим программу, которая использует все три интерфейса:

go
package main
 
import (
    "fmt"
    "io"
    "strings"
)
 
// Структура, которая реализует все три интерфейса
type DataProcessor struct {
    data string
    processed bool
}
 
// Реализуем io.Reader
func (dp *DataProcessor) Read(p []byte) (n int, err error) {
    if dp.processed {
        return 0, io.EOF
    }
    
    copy(p, []byte(dp.data))
    dp.processed = true
    return len(dp.data), nil
}
 
// Реализуем Stringer
func (dp DataProcessor) String() string {
    status := "не обработано"
    if dp.processed {
        status = "обработано"
    }
    return fmt.Sprintf("DataProcessor{data: %q, status: %s}", dp.data, status)
}
 
// Реализуем error
func (dp DataProcessor) Error() string {
    return fmt.Sprintf("ошибка обработки данных: %s", dp.data)
}
 
func main() {
    processor := &DataProcessor{data: "важные данные"}
    
    // Используем как Stringer
    fmt.Println("Состояние процессора:", processor)
    
    // Используем как io.Reader
    buffer := make([]byte, 100)
    n, err := processor.Read(buffer)
    if err != nil && err != io.EOF {
        fmt.Println("Ошибка чтения:", err)
    }
    fmt.Printf("Прочитано: %s\n", string(buffer[:n]))
    
    // Используем как error
    var errInterface error = processor
    fmt.Println("Как ошибка:", errInterface)
}

Этот пример показывает, как одна структура может реализовывать несколько интерфейсов одновременно. В реальном коде так делать не всегда нужно, но это демонстрирует гибкость системы интерфейсов Go.

Go Standard Library -- Interfaces
Source
--->
io.Reader
--->
Transform
--->
io.Writer
--->
Destination
SOURCE BYTES
bytes read: 0 / 660%
// last Read call n, err := r.Read(buf) // press Read(buf[8]) to start
RECEIVED CHUNKS
// no chunks yet
io.Copy -- SHORTCUT
io.Copy(dst, src) // reads all chunks in a loop // until io.EOF