Стандартные интерфейсы: io.Reader, Stringer, error
Что такое стандартные интерфейсы?
В Go есть несколько интерфейсов, которые встречаются повсюду в стандартной библиотеке. Их называют "стандартными" или "встроенными" интерфейсами. Сегодня разберем три самых важных: io.Reader, Stringer и error.
Знание этих интерфейсов поможет вам писать код, который хорошо интегрируется с остальной экосистемой Go.
io.Reader — интерфейс для чтения данных
io.Reader — это, пожалуй, самый распространенный интерфейс в 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().
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. Это один из немногих интерфейсов, который имеет глобальное значение в языке.
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)
}
}
}Создавать свои типы ошибок полезно, когда нужно передавать дополнительную информацию об ошибке или обрабатывать разные типы ошибок по-разному.
Практический пример: все вместе
Давайте создадим программу, которая использует все три интерфейса:
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.