Когда один файл — это уже тесно
Вы пишете на Rust. Ваш проект растёт. Ещё вчера main.rs вмещал всё — функции, структуры, пару конфигов. А сегодня вы тратите пять минут, чтобы найти нужную функцию среди тысячи строк. Знакомо?
Рано или поздно каждый разработчик Rust сталкивается с необходимостью разбить код на логические части. Казалось бы, что может быть проще? Создал папку, разложил файлы, импортировал — и готово. Но модульная система Rust славится своей строгостью. Одна неверная запись в mod.rs — и компилятор выдаёт ошибку, которую вы будете разгадывать полчаса.
В этой статье мы разберём топ ошибок при разделении модуля Rust на несколько файлов и, главное, покажем, как их избежать. Вы узнаете, как правильно организовать файловую структуру, когда использовать mod.rs, а когда — lib.rs, и почему pub use — ваш лучший друг.
Анатомия модульной системы Rust
Прежде чем нырять в ошибки, давайте вспомним основы. В Rust модуль — это единица организации кода. По умолчанию каждый файл — это модуль, но не всё так линейно.
Есть два подхода:
- Одноимённый файл-модуль: файл
utils.rsавтоматически становится модулемutils, если вы объявитеmod utils;вlib.rsилиmain.rs. - Директория-модуль: папка
utils/с файломmod.rsвнутри. В этом случаеmod.rs— точка входа в модуль.
Звучит просто, но именно здесь кроется большинство граблей. Давайте разберём самые частые.
Топ-5 ошибок при разбиении модуля на несколько файлов
Ошибка №1: Забыли объявить подмодуль
Вы создали файл network/tcp.rs и пытаетесь его использовать. Пишете use network::tcp; — и получаете ошибку компиляции. Почему? Потому что Rust не сканирует папки автоматически. Вы должны явно объявить каждый подмодуль.
// network/mod.rs
// ❌ Ошибка: подмодуль tcp не объявлен
// use network::tcp; // не скомпилируется
// ✅ Правильно:
pub mod tcp; // объявляем подмодуль
Как избежать: Всегда проверяйте, что вы добавили pub mod имя_подмодуля; в родительский модуль. Это похоже на явный импорт — Rust требует чёткости.
Ошибка №2: Путаница между mod.rs и lib.rs
Новички часто помещают mod.rs в корень проекта, рядом с main.rs. Это работает, но только если вы создаёте библиотеку. Для исполняемого файла (main.rs) корневой модуль — это сам main.rs. А mod.rs — это файл внутри папки.
my_project/
├── main.rs // корневой модуль для бинарника
├── lib.rs // корневой модуль для библиотеки (если есть)
├── utils/
│ ├── mod.rs // точка входа в модуль utils
│ └── helpers.rs
Как избежать: Запомните правило: mod.rs живёт только внутри папки-модуля. Если вам нужен корневой модуль для библиотеки — используйте lib.rs.
Ошибка №3: Забыли про pub в подмодулях
Вы объявили подмодуль, но всё равно не можете вызвать функцию. Скорее всего, вы забыли указать pub.
// network/mod.rs
pub mod tcp;
// network/tcp.rs
// ❌ Функция приватная по умолчанию
fn connect() { /* ... */ }
// ✅ Делаем её публичной
pub fn connect() { /* ... */ }
В Rust всё приватно по умолчанию. Это не баг, а фича: вы сами решаете, что открывать внешнему миру. Если функция нужна за пределами модуля — ставьте pub.
Как избежать: Используйте pub осознанно. Для внутренних утилит оставляйте приватный доступ. Для API — делайте публичным.
Ошибка №4: Игнорирование re-export (pub use)
Представьте: у вас есть модуль database с подмодулем models. Чтобы использовать структуру User, вам приходится писать use database::models::user::User;. Длинно и неудобно. А если структура переедет в другой файл — придётся менять все импорты.
Решение — re-export с помощью pub use.
// database/mod.rs
pub mod models;
// Re-export: делаем User доступным из database напрямую
pub use models::user::User;
// Теперь можно писать:
// use database::User; // вместо database::models::user::User
Это не только сокращает импорты, но и скрывает внутреннюю структуру модуля. Вы можете переименовать файлы внутри models, и внешний код не сломается — достаточно обновить pub use.
Как избежать: Сделайте pub use стандартной практикой для всех публичных типов. Это упрощает API и делает рефакторинг безболезненным.
Ошибка №5: Слишком глубокая вложенность
Вы создали структуру папок в 5 уровней: app/services/network/http/client.rs. Вроде логично, но теперь каждый импорт выглядит как эпопея. Кроме того, Rust ограничивает видимость: чтобы получить доступ к функции на 4-м уровне вложенности, нужно везде проставить pub.
// ❌ Слишком глубоко
use app::services::network::http::client::send_request;
// ✅ Лучше: выровнять модули
use app::http::client::send_request;
Как избежать: Придерживайтесь правила «не глубже 3 уровней». Если модуль становится слишком глубоким — подумайте, не пора ли вынести часть логики в отдельную библиотеку (crate).
Практический пример: разбиваем проект
Давайте соберём всё воедино. Допустим, вы пишете веб-сервер. Вот как может выглядеть правильная организация:
my_server/
├── main.rs
├── server/
│ ├── mod.rs
│ ├── listener.rs
│ └── handler.rs
├── config/
│ ├── mod.rs
│ └── settings.rs
└── utils/
├── mod.rs
├── logger.rs
└── validator.rs
Файл main.rs:
mod server;
mod config;
mod utils;
use server::