Как я за три недели переписал Econumo с PHP на Go и почти перестал читать код

· Read in English

PHP → Go

В декабре 2024 года я писал, что пересмотрел стек Econumo после трёх лет поддержки. Там была фраза: «Переписывать, конечно, я проект сейчас уже не возьмусь, но выводы на будущее сделал». Через полтора года я переписал весь проект. Бэкенд с PHP + Symfony на Go, фронтенд с Vue на React. Почти всю работу сделал Claude Code, а я его направлял и тестировал.

Расскажу, как это было и почему я теперь почти не читаю код, который попадает в продукт. Я пока не на 100% уверен, что такой подход применим к любому продукту, но в целом направление понятно.

Приложение, которое не планировалось переписывать

Econumo, это приложение для семейного бюджета, существует с 2021 года. Я собрал MVP за две недели на PHP, Symfony, PostgreSQL и Vue, потому что так быстрее, и потому что хотел заодно изучить Vue. С тех пор оно работало стабильно. Реальной причины что-то менять не было.

Работая над другими продуктами, я заметил, что почти перестал писать код руками. Его пишут агенты, а я ставлю задачи и проверяю результат. Но технологии я продолжал выбирать так, как будто пишу сам: что привычнее. А для агента критерии другие.

Что нужно агенту

Если код пишет человек, ему важны удобство, высокая читаемость и наличие инструментов. Агенту нужна обратная связь: быстрая, однозначная и желательно автоматическая. Чтобы он написал код, сразу получил ошибку компилятора, поправил, прогнал тесты, поправил ещё раз. Чем меньше слоёв магии между его правкой и сообщением об ошибке, тем лучше.

С этой точки зрения PHP мне перестал подходить. Вот несколько наиболее конкретных причин:

  • Интерпретируемость. Типы, PHPStan и IDE помогают, но часть проблем всё равно выплывает только когда код выполнился. У агента больше шансов пропустить ошибку.
  • Зависимости. Приложение держалось на Symfony и десятках пакетов: 40 прямых зависимостей и 107 пакетов в composer.lock. Я не обновлял многие из них около двух лет, потому что обновление к тому моменту стало задачей требующей кучи времени. Из прошлой статьи вы можете помнить, как Dependabot однажды принёс обновление, после которого сломалась авторизация.
  • Цепочка поставок. В пакетных экосистемах всё больше кода, который написан агентами. Проверить руками каждую зависимость нельзя. Чем их меньше, тем меньше поверхность, за которой надо следить.

Почему Go

Я хочу от языка: компиляция и быстрый компилятор, большая стандартная библиотека, встроенные тесты, простой синтаксис, один бинарник, малый расход ресурсов. Под это подходил Go.

  • Простота. Синтаксис небольшой и однообразный.
  • Компилятор. Невалидный код отлавливается сразу, а не в рантайме.
  • Стандартная библиотека. Роутер, HTTP, тесты, JSON, криптография есть из коробки. После миграции у бэкенда 9 прямых зависимостей против 40 в PHP версии.
  • Один бинарник. Его легко деплоить, откатывать и запускать в маленьком контейнере.

Rust я тоже рассматривал. Он безопаснее и быстрее, но заметно сложнее, а для этого приложения простота важнее максимального контроля.

Ещё одно открытие сделал уже по ходу. Организация Go приложения в пакеты очень хорошо подходит для работы с агентом. Когда каждая фича лежит в своём пакете со своими обработчиками, моделями, работой с БД и тестами, изменения остаются локальными. По списку затронутых пакетов видно, что именно агент менял, а если маленькая правка затронула десять несвязанных пакетов, это повод насторожиться. Структура пакетов стала ещё и способом ревью.

SQL тоже проходит проверку до запуска. Для работы с базой я использую sqlc: пишу (точнее, агент пишет) обычные SQL-запросы в файлах, а sqlc превращает их в типизированный Go-код. Схема берётся из тех же миграций, которые применяются при старте, поэтому запрос не может разойтись со схемой. Если агент написал запрос к колонке, которой нет, или переименовал колонку и забыл про запрос, генерация падает, и это видно сразу, ещё до тестов. Для двух баз (SQLite и PostgreSQL) генерируется по отдельному пакету из одних и тех же запросов, а там, где SQL действительно отличается, есть отдельные варианты. Сейчас в проекте около 580 именованных запросов, и каждый из них проверен компилятором и sqlc, а не только тестами.

Это ещё одна ступень обратной связи для агента: ошибка в SQL ловится так же быстро и однозначно, как ошибка в типах.

Триггер

Когда вышла модель Claude Fable я был впечатлён результатами экспериментов, и я решил попробовать ради спортивного интереса мигрировать проект с PHP на Go. Когда увидел, что работает, решил довести до конца.

Работал примерно по 20-30 часов в неделю три недели подряд, то есть порядка 60-90 часов. Первый PR с бэкендом открыт 20 июня, бета новой версии вышла 9 июля. Деньги ушли только на подписки: Claude Max 20x за $200 в месяц (взял самый большой план ради запаса лимитов под Fable) и Codex примерно за $25 в месяц. Подписки были на два месяца, но управился я примерно за один. Лимит на июль, кажется, я потратил полностью. Отдельных расходов на API не было.

В то время доступ к Fable был нестабильным: его объявляли доступным на неделю или две, потом продлевали. Всё это время я работал и переживал, что вот-вот доступ заберут, а я не успею закончить миграцию. Отчасти поэтому миграция двигалась быстро.

Сначала функционал, потом красота

Главное правило, которое я себе поставил: сначала добиться того же поведения, и только потом улучшать архитектуру. Если одновременно менять и логику, и структуру, непонятно, что сломалось.

  • Механический перевод. Агент перенёс PHP в Go почти один в один: те же эндпоинты, форматы запросов и ответов, бизнес-логика, поведение БД. Получилось не красиво, но предсказуемо.
  • Тесты. Перенёс и расширил. В PHP-версии было около 7 тысяч строк тестов. После миграции стало около 20 тысяч строк. Покрытие довёл до порога 80% и добавил проверку, которая требует, чтобы каждый новый маршрут попадал в каталог тестов.
  • Контракт API заморожен. Старый фронтенд не должен был заметить, что бэкенд сменился. Формат JWT, хеширование паролей, шифрование email проверялись по эталонным ответам живого PHP и на копии боевой БД.
  • Подмена на живом приложении. Go-бэкенд подключил к той же базе и тому же фронтенду. Миграция данных для пользователей не нужна: бинарник при старте узнаёт старые миграции Doctrine и применяет только новые.
  • Рефакторинг. Только когда поведение совпало, агент разложил проект по пакетам, убрал лишние абстракции и заменил часть сторонних пакетов своей реализацией.

Econumo поддерживает SQLite и PostgreSQL, поэтому одни и те же тесты прогоняются на обеих базах. Иначе агент легко внесёт изменение, которое работает на одной и ломается на другой.

Итог по архитектуре. Было слоёное приложение с элементами DDD: контроллеры, сервисы, репозитории, домен, инфраструктура. Стало приложение по фичам, где одна фича живёт в одном пакете.

Фронтенд: переписать, а не перенести

С фронтендом я поступил иначе. Старый Vue-код я писал, когда сам учил Vue, и там накопилась куча технического долга. Переносить как есть большого смысла не было.

Claude переписал большую часть почти с нуля на React. Он сначала смотрел на существующие экраны и поведение, воссоздавал структуру, подключал к неизменному API и прогонял UI и скриншот-тесты. Первый проход занял около 8 часов, всего агент работал около 15 часов. Потом я два-три дня просматривал все экраны руками и поправлял. Я использовал Chrome MCP для доступа к браузеру, чтобы Claude видел как было раньше.

Почему React? Для React больше всего примеров, компонентов и обсуждений, на которых учились модели, и агент в нём ошибается реже. Это тоже предположение, но практика его пока подтверждает. Размер кода почти не изменился: около 21 тысячи строк Vue и TypeScript превратились в 19 тысяч строк React.

К слову, после миграции, зависимостей во фронтенде стало больше, а не меньше: было 20, стало 38, так как очень много полезных библиотек типа Shadcn.

Что получилось в цифрах

Обе версии замерялись одинаково: Docker + пустая база SQLite, 16 потоков читают API в течение 15 секунд. PHP-образ последнего релиза (v0.10.1), Go-образ текущей версии (v1.7.0).

  • Образ. 1,59 ГБ против 66,5 МБ.
  • Память. PHP 57 мб после старта и до 100 мб под нагрузкой, Go 14 и 20 мб.
  • Скорость. PHP около 193 запросов в секунду при медианной задержке 79 мс. Go около 1190 запросов в секунду и 12 мс.

Забавный факт. Первая версия на Go (v1.0.0) занимала 6 МиБ в простое и отвечала быстрее, чем текущая (v1.7.0): за три месяца добавились MCP, мобильное приложение, регулярные транзакции, биллинг, и всё это не бесплатно.

Как я теперь ревьюю

Я перестал читать код фронтенда, а бэкенд читаю выборочно:

  • Какие пакеты и файлы изменились. Масштаб правки сразу виден. Маленькая фича с широким следом это тревожный знак.
  • Что стало с базой. Миграции, удалённые колонки, переименования, новые таблицы, странные индексы. Ошибку в БД сложнее исправить.

API-контракты, тесты, скриншоты, CI, Dependabot и ревью от моделей заменяют все остальное.

Для фронтенда скриншоты оказались ключевыми, потому что превращают «выглядит странно» в воспроизводимую проверку.

Режим наблюдателя: Claude пишет, Codex ревьюит

Основной агент у меня Claude Code. Но писать и проверять свой код одной и той же моделью, это такое себе. Поэтому я добавил в Claude skill с инструкцией: когда фича готова и проверена, он не докладывает «готово», а запускает ревью от Codex.

Как это работает:

  1. Claude доводит фичу, прогоняет тесты, сборку и линтеры.
  2. Создаёт feature-ветку, коммитит только файлы этой задачи и пушит.
  3. Запускает два ревью Codex параллельно: общее и отдельное, по безопасности.
  4. Читает замечания (они размечены от P0 до P3), сам проверяет каждое по коду. Настоящие чинит, ненастоящие отклоняет и объясняет почему.
  5. Повторяет, пока есть серьёзные замечания, но не больше трёх кругов.
  6. Отчитывается: что нашёл Codex, что исправлено, что отклонено. В основную ветку изменения попадают уже после этого, и PR он без моей команды не открывает.

Получается две модели от разных компаний, которые смотрят на один и тот же код с разных сторон, и я как последняя инстанция. Часть замечаний Codex оказывается false-positive, и Claude это сам видит. Но реальные находки бывают, и это на порядок дешевле, чем ловить их потом в продакшене.

Что было после

После выпуска v1.0.0 (11 июля) прошло около трёх с половиной месяцев. Вышло семь минорных версий, до v1.7.0. За это время добавились MCP-сервер, регулярные транзакции, мобильное приложение, подтверждение email, rate limiting, биллинг. Багов после переноса было мало.

Для сравнения, по датам публикации релизов на GitHub. За весь 2025 год на PHP вышло 3 релиза (v0.5.2, v0.6.0, v0.7.0). С января по май 2026 вышло 5 (от v0.8.0 до v0.10.1). После миграции, с 10 июля по 25 октября, за три с половиной месяца вышло 20 релизов, от беты v1.0.0 до v1.7.0.

Итого: примерно за месяц подписок (Claude $200, Codex около $25) я перенёс проект целиком и стал выпускать фичи чаще и эффективнее.

Это и есть для меня главный результат. Сокращение потребления ресурсов до 14 МБ вместо 250 - это приятный бонус. Важно, что я могу теперь развивать продукт очень быстро, в разы быстрее, чем раньше и с минимальными багами. Качество держится на: компиляторе, тестах, скриншотах, границы пакетов, и интеллекте моделей.

Выводы

  • Выбирайте технологии по качеству обратной связи для агента и по эффекту для продукта.
  • Минимум зависимостей. Каждая внешняя зависимость несёт риски.
  • При миграции сперва воспроизведите поведение, а только потом рефакторите.
  • Границы пакетов работают как границы безопасности.
  • Тесты это исполняемое знание о продукте, с ними реализацию можно менять целиком.
  • Самое пристальное внимание уделяйте мало-обратимому: миграциям и внешним контрактам.
  • Роль человека смещается вверх: что делаем, где риск, что нельзя ломать.

Код проекта открыт: github.com/econumo/econumo. Планы, по которым агент работал, лежат в docs/superpowers, а сами переписывания были в PR #46 (бэкенд) и #58 (фронтенд).

Если вы хотите вести семейный или личный бюджет, попробуйте Econumo. Есть бесплатный self-hosted вариант, а для тех, кому не хочется возиться с установкой, есть облачная версия с lifetime-покупкой. Это один из немногих продуктов, которым я сам пользуюсь на регулярной основе в последние 5 лет.

↑ Наверх