Какие архитектурные анти-паттерны вы встречали в Go-проектах и как их избегать?

Ответ

Хорошая архитектура нацелена на создание поддерживаемого, тестируемого и расширяемого кода. Вот несколько распространенных анти-паттернов в Go и способы их решения:

1. Глобальные переменные и неявное состояние Усложняют тестирование, приводят к неочевидным зависимостям и состоянию гонки в конкурентной среде.

Плохо:

var db *sql.DB // Глобальная переменная

func GetUser(id int) (*User, error) {
    // Функция неявно зависит от глобальной переменной db
    row := db.QueryRow("SELECT ...") 
    // ...
}

Как лучше: Использовать Dependency Injection (DI). Передавать зависимости явно через параметры конструктора или функции.

type UserRepo struct {
    db *sql.DB
}

func (r *UserRepo) GetUser(id int) (*User, error) {
    row := r.db.QueryRow("SELECT ...")
    // ...
}

2. Жесткая связность компонентов (Tight Coupling) Модули знают о конкретных реализациях друг друга, что усложняет замену компонентов и тестирование.

Плохо:

type OrderService struct {
    // Привязка к конкретной реализации MySQL
    storage *MySQLStorage 
}

Как лучше: Следовать Принципу инверсии зависимостей (DIP). Зависеть от абстракций (интерфейсов), а не от конкретных типов.

type OrderStorage interface {
    Save(Order) error
}

type OrderService struct {
    storage OrderStorage // Зависимость от интерфейса
}

3. Нарушение принципа единственной ответственности (SRP) Структура или пакет делают слишком много. Например, один модуль отвечает за логику, работу с БД и форматирование ответа.

Плохо:

// Генератор отчётов знает и о БД, и о форматах вывода
type ReportGenerator struct {
    db     *sql.DB
    pdfGen *PDFGenerator
    csvGen *CSVGenerator
}

Как лучше: Разделять обязанности. Один компонент получает данные, другой — форматирует их.

4. Чрезмерное использование interface{} Использование пустого интерфейса (any) там, где можно использовать конкретный тип или более строгий интерфейс, приводит к потере типобезопасности и усложняет код из-за постоянных проверок типа (type assertion).

Ответ 18+ 🔞

Ах, архитектура, ёпта! Это ж когда ты не просто хуяришь код, а думаешь, как потом с этим жить другим мудакам, включая себя через полгода. Смотри, какие бывают классические косяки в Go и как от них не обосраться.

1. Глобальные переменные — это пиздец как удобно, пока не начнёшь тестить Ну реально, объявил var db *sql.DB на уровне пакета и поехал — все функции ходят в эту общую миску, как поросята. А потом начинается: состояние гонки, тесты не изолированы, а когда надо заменить базу для теста — волнение ебать, потому что всё завязано на эту глобальную хуйню.

Как делают мудаки (не будь таким):

var db *sql.DB // Глобальная, блядь, переменная

func GetUser(id int) (*User, error) {
    // Функция молча сосёт из глобальной переменной, как будто так и надо
    row := db.QueryRow("SELECT ...") 
    // ...
}

Как сделать по-человечески: Засунуть зависимости в структуру, как в карман. Это называется Dependency Injection, но не пугайся, это не магия, а просто передача нужного дерьма явно.

type UserRepo struct {
    db *sql.DB // Вот она, зависимость, пришита намертво к экземпляру
}

func (r *UserRepo) GetUser(id int) (*User, error) {
    row := r.db.QueryRow("SELECT ...") // Берёт из своего, не шарит по глобальному
    // ...
}

2. Жёсткая связность — когда всё срослось, как сиамские близнецы Вот смотри: твой сервис заказов знает, что хранилище — это конкретно MySQL. А если завтра понадобится заглушка для тестов или переезд на PostgreSQL? Правильно, переписывать пол-проекта, потому что OrderService вротберунчик привязан к конкретной реализации.

Плохой пример (так не делай, а то получишь пизды):

type OrderService struct {
    // Жёсткая привязка, как удавка. Хранилище — только MySQL, и никаких гвоздей!
    storage *MySQLStorage 
}

Хороший подход (будь умным пид... то есть, разработчиком): Объяви интерфейс, от которого будет зависеть сервис. Сервису похуй, что там внутри — MySQL, мок или файлик на диске, главное, чтобы методы были.

type OrderStorage interface {
    Save(Order) error
}

type OrderService struct {
    storage OrderStorage // Теперь здесь может быть что угодно, реализующее интерфейс
}

3. Нарушение принципа единственной ответственности — когда один модуль тащит всё, как лошадь Представь себе генератор отчётов, который сам ходит в базу, сам конвертирует в PDF, сам в CSV, а ещё, наверное, чай делает. Это же пиздопроебибна! Такой монстр будет раздуваться, его невозможно тестировать, а если сломается PDF-генерация — падает весь модуль.

Кривой вариант (так писали в 90-х):

// Этот уродец знает всё на свете
type ReportGenerator struct {
    db     *sql.DB
    pdfGen *PDFGenerator
    csvGen *CSVGenerator
}
// А внутри ещё 500 строк логики на всё сразу

Как надо (разделяй и властвуй, блядь): Разбей на части: один компонент тащит данные, другой — рендерит в PDF, третий — в CSV. Каждый отвечает за свою одну хуйню, и если что-то сломалось — чинишь только один кирпич, а не всю стену.

4. Злоупотребление interface{} (или any) — это путь в ад без типобезопасности О, это любимая тема! Написал функцию, которая принимает interface{}, типа "универсально". А потом в ней куча проверок типов (value, ok := v.(string)), и если прилетело не то — получаешь панику или невнятную ошибку в рантайме. Зачем эта боль, если можно было сразу указать конкретный тип или узкий интерфейс?

Запомни: interface{} — это как сиськи в баре: все на них смотрят, но использовать нужно с осторожностью и только когда действительно надо. В 90% случаев есть способ обойтись без этого.