Контекст
30 января. server.go разросся до 700+ строк. В нём перемешано всё: управление клиентами, аутентификация, пул стримов, аллокация портов, lifecycle сервера. Каждый раз, когда добавляю фичу — лезу в один файл и молюсь, что ничего не сломаю.
Пора навести порядок.
Проблема: God Object
server.go стал тем, что называют God Object — объект, который знает и делает всё. Хочешь понять, как работает аутентификация? Ищи в server.go. Управление клиентами? Там же. Пул стримов? Угадайте где.
Три проблемы:
1. Сложно читать — 700 строк в одном файле, логика перемешана
2. Сложно тестировать — чтобы протестировать аутентификацию, нужно поднять весь сервер
3. Сложно менять — одно изменение может задеть что угодно
Как разбивал
Принцип простой: каждая ответственность — отдельный файл и структура.
ClientManager
Управление подключёнными клиентами: добавить, удалить, найти по ID, найти по поддомену. Раньше это были методы Server с доступом к общей map'е через мьютекс. Теперь — отдельная структура со своим мьютексом.
internal/server/client_manager.go — 232 строки
Что даёт: сервер не знает, как хранятся клиенты. Может быть map, может быть Redis (когда-нибудь). ClientManager — единственная точка входа.
AuthHandler
Обработка аутентификации при подключении клиента: проверка токена, валидация прав, IP whitelist. Раньше — метод handleAuth на 100 строк внутри сервера. Теперь — отдельная структура.
internal/server/auth_handler.go — 272 строки
StreamPool
Пул предварительно открытых yamux-стримов (из статьи 8 про производительность). Логика была встроена прямо в методы сервера. Вынес в отдельный файл.
internal/server/stream_pool.go — 72 строки
PortAllocator
TCP и UDP менеджеры оба занимались аллокацией портов. Одинаковый код в двух местах. Вынес общую логику в PortAllocator, оба менеджера используют его.
internal/server/port_allocator.go — 60 строк
TCP-менеджер: было 160 строк, стало ~95. UDP-менеджер: аналогично. Дублирование убито.
Результат
de66ef7 refactor(server): extract client manager, auth handler, stream pool
— 590 добавлено, 502 удалено
635cbad refactor(server): extract shared port allocator
— 204 добавлено, 101 удалено
server.go похудел с 700+ строк до ~200. Остался только lifecycle: Start, Stop, accept connections. Всё остальное — в специализированных компонентах.
Заодно: наводим порядок в коде
Параллельно с рефакторингом сервера прошёлся по всему проекту:
Magic numbers → именованные константы (a196103). Вместо time.Sleep(5 * time.Second) — time.Sleep(shutdownDrainTimeout). Вместо 1 << 20 — MaxMessageSize. Код читается как текст, а не как набор загадочных чисел.
Стандартизация ошибок в репозиториях (b48ff9e). Каждый репозиторий обрабатывал ошибки по-своему: где-то sql.ErrNoRows пробрасывался наружу, где-то заменялся на nil. Привёл к единому паттерну: ErrNotFound для всех.
Линтеры (3fabb70, 9c66b9c). Включил errcheck (проверка обработки ошибок) и security-линтеры (gosec). Починил все предупреждения — 20+ файлов. Те самые errcheck, которые я отключил в первый день. Теперь — включены.
Грабли
Мьютексы при извлечении. ClientManager получил свой мьютекс, но сервер всё ещё обращался к клиентам напрямую в паре мест. Дедлок. Пришлось пройтись по всем вызовам и убедиться, что доступ только через ClientManager.
Тесты после рефакторинга. Существующие тесты обращались к внутренним полям сервера. После рефакторинга полей нет — они в ClientManager. Пришлось обновить тесты, добавить геттеры.
Порядок блокировок. С несколькими мьютексами появляется риск дедлока: горутина A берёт мьютекс 1, ждёт мьютекс 2; горутина B берёт мьютекс 2, ждёт мьютекс 1. Задокументировал порядок блокировок в комментариях (b416eef).
Итог
30 января:
- server.go: 700+ → ~200 строк
- 4 новых файла с чёткой ответственностью
- Общий PortAllocator вместо дублирования
- Именованные константы вместо magic numbers
- Единообразная обработка ошибок
- Включены и починены все линтеры
de66ef7 refactor(server): extract client manager, auth handler, stream pool
635cbad refactor(server): extract shared port allocator
a196103 refactor: replace magic numbers with named constants
b48ff9e refactor(database): standardize error handling
3fabb70 build(lint): enable errcheck and add security linters
9c66b9c fix(lint): resolve errcheck and gosec warnings
Код стал читаемым, тестируемым и поддерживаемым. Скучно? Может быть. Но именно такая работа отличает код, который живёт, от кода, который через полгода придётся переписать.
В следующей части: полировка продукта — graceful shutdown, trace ID, авто-обновление клиента и кнопка «replay» для инспекции трафика.