Skip to main content

Интерфейсы и структуры данных

Что такое неэкспортируемая пустая структура

Интерфейс потребитель, а не производитель

Кастомные ошибки в Go

Что такое неэкспортируемая пустая структура

Это структура, определённая как 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).