Proxy Pattern в Go: Контроль доступа и управление ресурсами
Proxy pattern предоставляет суррогат или заместитель другого объекта для контроля доступа к нему. Он действует как посредник, добавляя функциональность без изменения оригинального объекта.
Вот когда и как использовать его в реальных Go-приложениях.
Проблема
У вас есть объект, который дорого создавать, требует контроля доступа или нуждается в дополнительной функциональности. Прямой доступ вызывает проблемы:
Сценарий 1: Подключение к базе данных
Ваше приложение подключается к PostgreSQL. Каждое соединение потребляет память и сетевые ресурсы. Создание соединений для каждого запроса расточительно. Нужен пул соединений, но клиенты не должны знать об этом.
Сценарий 2: Внешний API
Вы вызываете сторонний платёжный API. Каждый вызов стоит денег. Нужно кеширование, ограничение частоты запросов и логирование. Но вы не хотите модифицировать код API-клиента каждый раз при изменении требований.
Сценарий 3: Чувствительные ресурсы
Ваш сервис обращается к пользовательским данным. Не все пользователи имеют право доступа ко всем данным. Нужны проверки авторизации перед каждым доступом. Но бизнес-логика не должна быть загромождена кодом безопасности.
Общая нить: Нужно контролировать доступ к объекту без изменения того, как клиенты его используют.
Что такое Proxy Pattern
Proxy pattern создаёт объект-посредник, который:
- Реализует тот же интерфейс, что и реальный объект
- Контролирует доступ к реальному объекту
- Может добавлять функциональность до/после делегирования реальному объекту
- Выглядит идентично для клиентов
Ключевое понимание: Клиенты не знают, что общаются с прокси. Они думают, что используют реальный объект.
Типы прокси
Virtual Proxy (Виртуальный прокси)
Цель: Отложить дорогостоящее создание объекта до момента реальной необходимости.
Реальный случай: Загрузка изображений в системе управления контентом.
Вы строите CMS, где статьи содержат изображения. Загрузка всех изображений сразу приведёт к:
- Чрезмерному потреблению памяти
- Замедлению рендеринга страницы
- Трате трафика на изображения, которые пользователь никогда не увидит
Решение с виртуальным прокси:
- Создать лёгкий заполнитель для каждого изображения
- Загружать реальное изображение только когда пользователь прокручивает до него
- Кешировать загруженные изображения для повторного использования
- Освобождать память для изображений, мимо которых прокрутили
Преимущества:
- Быстрая начальная загрузка страницы
- Меньшее потребление памяти
- Лучший пользовательский опыт
- Снижение нагрузки на сервер
Protection Proxy (Защитный прокси)
Цель: Контролировать доступ на основе разрешений.
Реальный случай: Система управления документами.
В вашей компании есть конфиденциальные документы. Разные сотрудники имеют разные уровни доступа:
- Публичные документы: Все могут читать
- Внутренние документы: Только сотрудники
- Конфиденциальные: Только менеджеры
- Секретные: Только топ-менеджмент
Решение с защитным прокси:
- Проверять права пользователя перед разрешением доступа
- Логировать все попытки доступа для аудита
- Отказывать в доступе с понятными сообщениями об ошибках
- Поддерживать ролевой контроль доступа (RBAC)
Преимущества:
- Централизованная логика безопасности
- Согласованный контроль доступа
- Аудиторский след для соответствия требованиям
- Легко изменять права доступа
Remote Proxy (Удалённый прокси)
Цель: Представлять объект в другом адресном пространстве.
Реальный случай: Коммуникация микросервисов.
Ваша e-commerce платформа имеет отдельные сервисы:
- Сервис продуктов (управляет инвентарём)
- Сервис заказов (обрабатывает заказы)
- Сервис платежей (обрабатывает транзакции)
Сервису заказов нужна информация о продуктах, но он не должен напрямую обращаться к базе данных продуктов. Решение с удалённым прокси:
- Сервис заказов использует прокси, который выглядит как локальный репозиторий продуктов
- Прокси делает HTTP/gRPC вызовы к сервису продуктов
- Обрабатывает сетевые ошибки и повторные попытки
- Кеширует ответы для уменьшения сетевых вызовов
Преимущества:
- Сервисы остаются разделёнными
- Сетевая сложность скрыта от бизнес-логики
- Легко добавить кеширование и логику повторов
- Можно переключаться между локальной и удалённой реализациями
Caching Proxy (Кеширующий прокси)
Цель: Кешировать дорогостоящие операции.
Реальный случай: Аналитический дашборд.
Ваш дашборд показывает бизнес-метрики:
- Выручка по регионам (сложный SQL-запрос)
- Рост пользователей (агрегирует миллионы записей)
- Производительность продуктов (объединяет множество таблиц)
Запросы занимают 5-10 секунд каждый. Пользователи часто обновляют страницу. База данных перегружена.
Решение с кеширующим прокси:
- Прокси находится между дашбордом и базой данных
- Первый запрос: Выполнить запрос, закешировать результат
- Последующие запросы: Вернуть закешированный результат
- Инвалидировать кеш через 5 минут или при обновлении данных
Преимущества:
- Дашборд отвечает мгновенно
- Нагрузка на базу данных снижена на 90%
- Лучший пользовательский опыт
- Меньшие затраты на инфраструктуру
Реальная реализация: Database Proxy
Контекст
Вы строите SaaS-приложение. Множество арендаторов делят одну базу данных. Данные каждого арендатора должны быть изолированы. Вам нужно:
- Автоматическая фильтрация по арендатору во всех запросах
- Логирование запросов для отладки
- Пул соединений для производительности
- Автоматический повтор при временных сбоях
Без прокси
Каждый вызов базы данных в вашем коде выглядит так:
// Разбросано по всему коду
func GetUser(db *sql.DB, tenantID, userID string) (*User, error) {
// Проверка арендатора - повторяется везде
if tenantID == "" {
return nil, errors.New("требуется арендатор")
}
// Логирование - повторяется везде
log.Printf("Запрос: GetUser, Арендатор: %s, Пользователь: %s", tenantID, userID)
// Реальный запрос
row := db.QueryRow("SELECT * FROM users WHERE tenant_id = $1 AND id = $2", tenantID, userID)
// Обработка ошибок - повторяется везде
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
log.Printf("Ошибка: %v", err)
return nil, err
}
return &user, nil
}
Проблемы:
- Проверка арендатора дублируется в каждой функции
- Легко забыть фильтрацию по арендатору (риск безопасности!)
- Код логирования загромождает бизнес-логику
- Нельзя добавить функции без модификации всех функций
С прокси
Прокси обрабатывает сквозные задачи:
type DatabaseProxy struct {
db *sql.DB
tenantID string
logger *log.Logger
}
func (p *DatabaseProxy) QueryRow(query string, args ...interface{}) *sql.Row {
// Автоматическая инъекция арендатора
modifiedQuery := p.injectTenantFilter(query)
// Логирование
p.logger.Printf("Запрос: %s, Арендатор: %s", query, p.tenantID)
// Делегирование реальной базе данных
return p.db.QueryRow(modifiedQuery, args...)
}
Теперь ваша бизнес-логика чиста:
func GetUser(db *DatabaseProxy, userID string) (*User, error) {
// Проверка арендатора не нужна - прокси обрабатывает
// Логирование не нужно - прокси обрабатывает
row := db.QueryRow("SELECT * FROM users WHERE id = $1", userID)
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
return &user, err
}
Преимущества:
- Бизнес-логика фокусируется на бизнесе
- Безопасность обеспечивается автоматически
- Легко добавлять функции (кеширование, метрики, трейсинг)
- Единое место для модификации поведения базы данных
Реальная реализация: API Client Proxy
Контекст интеграции
Ваше приложение интегрируется с платёжным API Stripe. Требования:
- Ограничение частоты запросов (100 запросов в минуту)
- Автоматический повтор при сетевых ошибках
- Кеширование для GET-запросов
- Логирование всех API-вызовов для отладки
Circuit breakerдля предотвращения каскадных сбоев
Без паттерна прокси
Каждый API-вызов нуждается во всей этой логике:
func ChargeCustomer(client *stripe.Client, amount int) error {
// Проверка ограничения частоты
if !rateLimiter.Allow() {
return errors.New("превышен лимит запросов")
}
// Логирование
log.Printf("Списание с клиента: %d", amount)
// Логика повторов
var err error
for i := 0; i < 3; i++ {
err = client.Charge(amount)
if err == nil {
break
}
time.Sleep(time.Second * time.Duration(i+1))
}
// Проверка circuit breaker
if err != nil {
circuitBreaker.RecordFailure()
}
return err
}
Эта логика дублируется для каждого метода API. Кошмар поддержки.
С паттерном прокси
Прокси инкапсулирует все сквозные задачи:
type StripeProxy struct {
client *stripe.Client
rateLimiter *RateLimiter
cache *Cache
circuitBreaker *CircuitBreaker
logger *log.Logger
}
func (p *StripeProxy) Charge(amount int) error {
// Проверка circuit breaker
if p.circuitBreaker.IsOpen() {
return errors.New("circuit breaker открыт")
}
// Проверка ограничения частоты
if !p.rateLimiter.Allow() {
return errors.New("ограничение частоты")
}
// Логирование запроса
p.logger.Printf("Stripe списание: %d", amount)
// Повтор с экспоненциальной задержкой
err := p.retryWithBackoff(func() error {
return p.client.Charge(amount)
})
// Обновление circuit breaker
if err != nil {
p.circuitBreaker.RecordFailure()
} else {
p.circuitBreaker.RecordSuccess()
}
return err
}
Бизнес-код остаётся простым:
func ProcessPayment(proxy *StripeProxy, amount int) error {
// Вся сложность обработана прокси
return proxy.Charge(amount)
}
Преимущества:
- Функции надёжности в одном месте
- Легко тестировать (мокировать прокси)
- Можно менять реализации (тест vs продакшн)
- Бизнес-логика остаётся чистой
Реальная реализация: Lazy Loading Proxy
Контекст загрузки данных
Ваше приложение загружает профили пользователей. Каждый профиль включает:
- Базовую информацию (имя, email) - маленькая, быстрая
- Аватар - большой, медленно загружается
- История активности - огромная, дорогой запрос
- Настройки - средний размер
Большинство запросов нуждаются только в базовой информации. Загрузка всего тратит ресурсы.
Решение с ленивой загрузкой
Прокси с ленивой загрузкой загружает данные по требованию:
type UserProxy struct {
userID string
repo *UserRepository
// Кешированные данные
basicInfo *BasicInfo
avatar *Image
history *ActivityHistory
preferences *Preferences
// Флаги загрузки
basicLoaded bool
avatarLoaded bool
historyLoaded bool
prefsLoaded bool
}
func (p *UserProxy) GetBasicInfo() *BasicInfo {
if !p.basicLoaded {
p.basicInfo = p.repo.LoadBasicInfo(p.userID)
p.basicLoaded = true
}
return p.basicInfo
}
func (p *UserProxy) GetAvatar() *Image {
if !p.avatarLoaded {
p.avatar = p.repo.LoadAvatar(p.userID)
p.avatarLoaded = true
}
return p.avatar
}
Паттерны использования:
Паттерн 1: Список (только базовая информация)
users := GetUsers() // Возвращает прокси
for _, user := range users {
// Загружена только базовая информация
fmt.Printf("%s (%s)\n", user.GetBasicInfo().Name, user.GetBasicInfo().Email)
}
// Аватар и история никогда не загружались - сэкономлено время и память
Паттерн 2: Детальный вид (всё)
user := GetUser(userID) // Возвращает прокси
// Загрузка по требованию по мере необходимости
info := user.GetBasicInfo()
avatar := user.GetAvatar()
history := user.GetHistory()
Преимущества:
- Быстрее списки (на 90%)
- Меньше использование памяти (снижение на 70%)
- Снижена нагрузка на базу данных
- Тот же интерфейс для клиентов
Когда использовать Proxy Pattern
Используйте когда:
Нужен контроль доступа:
- Аутентификация/авторизация
- Аудит-логирование
- Ограничение частоты запросов
Требуется управление ресурсами:
- Ленивая загрузка
- Пул соединений
- Кеширование
Дополнительная функциональность без модификации:
- Логирование
- Метрики
- Трейсинг
Удалённый доступ:
- Коммуникация микросервисов
- API-клиенты
- Распределённые системы
Не используйте когда:
Достаточен простой прямой доступ:
- Не нужен контроль доступа
- Нет проблем с производительностью
- Не требуется дополнительная функциональность
Накладные расходы не оправданы:
- Очень простые объекты
- Критичный путь производительности
- Сложность перевешивает преимущества
Proxy vs Decorator
Оба добавляют функциональность, но разные цели:
Proxy:
- Контролирует доступ к объекту
- Может не создавать реальный объект немедленно
- Фокусируется на контроле доступа и управлении ресурсами
- Клиент может не знать, что использует прокси
Decorator:
- Добавляет функциональность объекту
- Реальный объект всегда существует
- Фокусируется на расширении поведения
- Клиент явно оборачивает объект
Пример различия:
Proxy: “Проверить, есть ли у пользователя разрешение перед доступом к базе данных” Decorator: “Добавить шифрование к записи файлов”
Распространённые ошибки
Ошибка 1: Прокси становится god object
Не добавляйте несвязанную функциональность в прокси. Держите его сфокусированным.
Плохо: Прокси обрабатывает кеширование, логирование, метрики, валидацию, трансформацию и бизнес-логику.
Хорошо: Отдельные прокси для отдельных задач. Цепочка их при необходимости.
Ошибка 2: Забыть реализовать все методы интерфейса
Если прокси не реализует полный интерфейс, клиенты ломаются.
Решение: Используйте встраивание интерфейсов в Go для обеспечения полноты.
Ошибка 3: Накладные расходы на производительность
Каждый вызов прокси добавляет накладные расходы. Не используйте прокси в горячих путях без необходимости.
Решение: Сначала профилируйте. Добавляйте прокси только там, где преимущества перевешивают затраты.
Тестирование с прокси
Прокси упрощают тестирование:
Тестирование поведения прокси:
func TestCachingProxy(t *testing.T) {
mock := &MockDatabase{}
proxy := NewCachingProxy(mock)
// Первый вызов обращается к базе данных
proxy.GetUser("user-1")
assert.Equal(t, 1, mock.CallCount)
// Второй вызов использует кеш
proxy.GetUser("user-1")
assert.Equal(t, 1, mock.CallCount) // Всё ещё 1!
}
Тестирование с mock прокси:
func TestBusinessLogic(t *testing.T) {
mockProxy := &MockDatabaseProxy{
Users: map[string]*User{
"user-1": {ID: "user-1", Name: "Тест"},
},
}
// Тестирование бизнес-логики без реальной базы данных
result := ProcessUser(mockProxy, "user-1")
assert.NotNil(t, result)
}
Заключение
Proxy pattern предоставляет контролируемый доступ к объектам.
Ключевые преимущества:
- Разделение ответственности
- Централизованная сквозная логика
- Легко добавлять функциональность
- Прозрачен для клиентов
Распространённые случаи использования:
- Контроль доступа и безопасность
- Управление ресурсами и оптимизация
- Доступ к удалённым объектам
- Ленивая загрузка и кеширование
Советы по реализации:
- Держите прокси сфокусированными
- Реализуйте полный интерфейс
- Учитывайте влияние на производительность
- Используйте для сквозных задач
Proxy pattern необходим для построения поддерживаемых, безопасных и производительных Go-приложений.
Как вы используете прокси в ваших Go-приложениях? Делитесь опытом в комментариях или пишите напрямую.