Ответ
Хорошая архитектура нацелена на создание поддерживаемого, тестируемого и расширяемого кода. Вот несколько распространенных анти-паттернов в 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% случаев есть способ обойтись без этого.