Интерфейсы и структуры данных
Что такое неэкспортируемая пустая структура
Интерфейс потребитель, а не производитель
Что такое неэкспортируемая пустая структура
Это структура, определённая как struct{}, без полей и с именем, начинающимся со строчной буквы, чтобы ограничить область видимости внутри пакета.
Зачем она нужна
• Неэкспортируемую пустую структуру часто используют, чтобы обозначить, что тип реализует определённый интерфейс, или для добавления семантики без хранения состояния.
• Так как struct{} занимает 0 байт, её используют в ситуациях, где нужно просто присутствие элемента без данных:
set := map[string]struct{}{}
set["apple"] = struct{}{}
• Пустая структура идеальна для каналов, передающих только сигнал, а не данные:
done := make(chan struct{})
go func() {
// работа
done <- struct{}{}
}()
<-done
• Если структура неэкспортируема, она не видна за пределами пакета — удобно для инкапсуляции логики, когда внешний код должен работать только через интерфейсы.
Интерфейс потребитель, а не производитель
Одна из классических ошибок начинающих Go-разработчиков — определять интерфейсы слишком рано и слишком широко. Интерфейс должен описывать поведение, которое вам нужно, а не всё, что умеет делать структура.
Плохо:
type Database interface {
Create()
Read()
Update()
Delete()
}
Здесь мы создали толстый интерфейс со всеми возможными операциями. Но что если вашему коду нужно только читать данные?
Хорошо:
type Reader interface {
Read() ([]byte, error)
}
Узкий интерфейс, описывающий ровно то, что нужно конкретному потребителю.
Думайте как потребитель: «Что мне нужно от этой зависимости?» — и определяйте интерфейс исходя из ответа. Это чистый YAGNI для интерфейсов — не определяй методы, пока они не нужны.
Кастомные ошибки в Go
Встроенный интерфейс error — это минимализм. Но когда приложение растёт, одного текста сообщения становится недостаточно. Нужно знать, что сломалось, почему это произошло и как это обработать.
Кастомный тип ошибки — это структура, которая содержит не только сообщение, но и дополнительные данные. Это позволяет обработчику ошибки принять осознанное решение: может, это ошибка валидации и стоит показать пользователю сообщение о неправильном вводе.
Как это выглядит на практике:
// Определяем свой тип ошибки
type InvalidInputError struct {
FieldName string
Value string
}
// Реализуем интерфейс Error()
func (e *InvalidInputError) Error() string {
return fmt.Sprintf("invalid input for field '%s': '%s'", e.FieldName, e.Value)
}
// Используем его в функции
func processInput(input string) error {
if len(input) == 0 {
return &InvalidInputError{FieldName: "input", Value: input}
}
return nil
}
// А вот и обработка
func main() {
err := processInput("")
if err != nil {
if invalidErr, ok := err.(*InvalidInputError); ok {
fmt.Printf("Validation error: %s (Field: %s)\n", invalidErr.Error(), invalidErr.FieldName)
} else {
fmt.Printf("Unexpected error: %s\n", err)
}
}
}
Мы поймали ошибку через type assertion и получили доступ к полям структуры. Это даёт намного больше гибкости, чем просто if err != nil.
В современном Go для распаковки обёрнутых ошибок есть errors.As() — это удобнее, чем type assertion, особенно если ошибку кто-то обернул через fmt.Errorf("%w", err).
No comments to display
No comments to display