fxTunnel

· Часть 12 серии «Пишу свой ngrok на Go: дневник разработки»

Пишу свой ngrok на Go: рефакторинг — разбиваем монолит

В нём перемешано всё: управление клиентами, аутентификация, пул стримов, аллокация портов, lifecycle сервера.

Контекст

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 << 20MaxMessageSize. Код читается как текст, а не как набор загадочных чисел. Стандартизация ошибок в репозиториях (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» для инспекции трафика.

Все части дневника разработки