В Rust единица компиляции — крейт (crate): бинарный (main.rs) или библиотечный (lib.rs). Внутри крейта пространство имён организовано в дерево модулей через ключевое слово mod. В отличие от Python или Go, где файл автоматически становится модулем/пакетом, в Rust модуль нужно явно объявить — компилятор не сканирует директории сам.
// src/lib.rs — корень крейта
mod network {
pub fn connect() {
println!("connecting...");
}
mod tcp {
pub fn handshake() {}
}
}
fn main() {
// Полный путь от корня крейта
crate::network::connect();
}
Начиная с Rust 2018, модуль можно вынести в отдельный файл двумя способами: старый стиль mod.rs и современный стиль "имя файла = имя модуля".
// Стиль Rust 2015/2018 (устаревающий, но всё ещё встречается):
// src/network/mod.rs — сам модуль network
// src/network/tcp.rs — подмодуль network::tcp
// Современный стиль (Rust 2018+, рекомендуемый):
// src/network.rs — сам модуль network
// src/network/tcp.rs — подмодуль network::tcp
// В src/lib.rs объявление одинаковое в обоих случаях:
mod network; // компилятор ищет network.rs ИЛИ network/mod.rs
mod.rs в IDE. Файл network.rs сразу говорит, какому модулю принадлежит, чего не скажешь про безликий mod.rs. С Rust 2018 это рекомендуемый подход, mod.rs оставлен для обратной совместимости.
По умолчанию всё приватно относительно родительского модуля — это противоположность Go, где видимость определяется регистром первой буквы идентификатора. В Rust видимость — явный модификатор.
mod storage {
pub struct Database {
pub name: String, // видно снаружи модуля
connection_pool: u32, // приватно (по умолчанию)
}
pub(crate) fn internal_reset() {
// видно во всём крейте, но не за его пределами —
// удобно для внутренних API, не часть публичного контракта
}
mod engine {
pub(super) fn flush() {
// видно только в родительском модуле storage,
// не дальше — узкая протечка API вглубь дерева
}
}
}
use создаёт короткий алиас на путь в дереве модулей — аналог import в Python или Go, но с более гибкой системой путей: self, super, crate, имя внешнего крейта.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet, BTreeMap}; // группировка
use std::io::Result as IoResult; // переименование при импорте
use std::collections::hash_map::Entry::*; // glob-импорт (осторожно!)
mod shapes {
pub struct Circle;
pub struct Square;
}
// Re-export: делаем shapes::Circle доступным как crate::Circle
pub use shapes::Circle;
pub use shapes::Square;
pub use — идиоматичный способ построить плоский публичный API поверх глубоко вложенной внутренней структуры модулей. Это то, что в других экосистемах решается через __init__.py (Python) или явные ре-экспорты в index.ts (TypeScript/npm-пакеты).
// src/lib.rs крупной библиотеки — типичный prelude-паттерн
mod engine;
mod renderer;
mod physics;
// Плоский публичный API вместо myapp::engine::core::Engine
pub use engine::Engine;
pub use renderer::Renderer;
pub use physics::{RigidBody, Collider};
// Потребитель пишет:
// use myapp::{Engine, Renderer, RigidBody};
// а не use myapp::engine::core::Engine;
use module::*; удобен в тестах (use super::*;) и прелюдиях, но в остальном коде затрудняет отслеживание происхождения идентификаторов и провоцирует конфликты имён при добавлении новых элементов в исходный модуль. Предпочитайте явный список импортов.
Cargo.toml — аналог package.json в npm, go.mod в Go или pyproject.toml в Python: единая точка описания метаданных, зависимостей и конфигурации сборки.
[package]
name = "awesome-service"
version = "0.3.1"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "HTTP-сервис обработки заказов"
repository = "https://github.com/example/awesome-service"
readme = "README.md"
rust-version = "1.75" # минимальная поддерживаемая версия (MSRV)
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
reqwest = "0.12"
internal-utils = { path = "../internal-utils" } # локальная зависимость
[dev-dependencies]
mockito = "1.4" # доступно только в cargo test
criterion = "0.5" # бенчмарки
[build-dependencies]
cc = "1.0" # доступно только для build.rs
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
build.rs, компилируются отдельно под хост-платформу (актуально при кросс-компиляции)replace в Go modules или file: в npmWorkspace — механизм Cargo для объединения нескольких пакетов под общим корнем с единым Cargo.lock и общей директорией target/. Близкий аналог — Go workspaces (go.work, с Go 1.18) или npm/yarn workspaces в JS-экосистеме.
# Cargo.toml в корне репозитория
[workspace]
resolver = "2"
members = [
"crates/api-server",
"crates/domain",
"crates/storage",
"crates/cli",
]
exclude = ["crates/experimental"]
# Общие метаданные, наследуемые дочерними Cargo.toml (Cargo 1.64+)
[workspace.package]
version = "1.2.0"
edition = "2021"
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# crates/api-server/Cargo.toml — дочерний пакет
[package]
name = "api-server"
version.workspace = true # наследуем из [workspace.package]
edition.workspace = true
[dependencies]
serde.workspace = true # версия и features из [workspace.dependencies]
tokio.workspace = true
domain = { path = "../domain" }
Все члены workspace разделяют один Cargo.lock в корне — это гарантирует, что во всех крейтах используется одна и та же версия каждой транзитивной зависимости, и исключает дублирование в конечном бинарнике. Команда cargo build, запущенная из корня, собирает все члены с переиспользованием артефактов компиляции.
[workspace.package] и [workspace.dependencies] (Cargo 1.64+) избавляют от рассинхронизации версий зависимостей между десятком внутренних крейтов — классической боли в растущих монорепозиториях.
Решение зависит от того, как код будет развиваться, тестироваться и публиковаться.
| Один крейт | Workspace |
|---|---|
Один Cargo.toml, один Cargo.lock | Корневой Cargo.toml с [workspace] + Cargo.lock общий на всех членов |
| Подходит для небольшой библиотеки или сервиса | Подходит для монорепозитория: сервис + внутренние библиотеки + CLI-утилиты |
| Нет переиспользования артефактов сборки между проектами | Общий target/ — зависимости компилируются один раз для всех членов |
| Публикация на crates.io — единственный вариант деления по границе публикации | Каждый member публикуется на crates.io независимо своей командой cargo publish |
| Простая ментальная модель, минимум конфигурации | Единая версия транзитивных зависимостей у всех членов — меньше конфликтов semver |
| Изменение затрагивает только один репозиторий/пакет | Атомарные изменения сразу в нескольких взаимосвязанных крейтах в одном PR |
| CI собирает и тестирует всё целиком | Можно точечно собирать/тестировать один member: cargo test -p domain |
Типичный сигнал к переходу на workspace — когда в проекте появляется вторая переиспользуемая часть (например, доменная логика, нужная и HTTP-серверу, и CLI-утилите, и воркеру очереди), либо когда команда хочет публиковать несколько связанных крейтов на crates.io (как tokio + tokio-util + tokio-stream).
Cargo features — механизм условной компиляции кода и опциональных зависимостей. Ближайший аналог — build tags в Go или extras в Python (pip install package[extra]), но с более строгими гарантиями через unification.
# Cargo.toml
[features]
default = ["json"]
json = ["dep:serde_json"]
xml = ["dep:quick-xml"]
full = ["json", "xml", "metrics"]
metrics = []
[dependencies]
serde_json = { version = "1.0", optional = true }
quick-xml = { version = "0.31", optional = true }
#[cfg(feature = "json")]
pub fn to_json<T: serde::Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap()
}
#[cfg(feature = "xml")]
pub fn to_xml<T>(value: &T) -> String {
// ...
todo!()
}
Подключение конкретных features у зависимости:
[dependencies]
awesome-lib = { version = "2.0", default-features = false, features = ["xml"] }
Если в дереве зависимостей несколько крейтов используют один и тот же пакет с разными наборами features, Cargo объединяет их (unification) — итоговый пакет собирается один раз с union всех запрошенных features по всему workspace/графу зависимостей.
tokio с features = ["full"], а вы хотели минимальный набор — вы всё равно получите полный набор, потому что features глобальны для сборки. Возможные решения: mutually exclusive features избегать в публичных крейтах, использовать resolver = "2" (разделяет unification для build/dev-dependencies и target-specific зависимостей), либо feature-флаги делать строго аддитивными и без взаимоисключающей логики.
Cargo строго следует SemVer (MAJOR.MINOR.PATCH) и использует его как контракт совместимости при разрешении версий. Это отличает экосистему от npm, где semver — соглашение, часто нарушаемое, и от Go modules, где major-версии ≥ 2 кодируются прямо в пути импорта (module/v2).
[dependencies]
serde = "1.0.200" # эквивалентно ^1.0.200 — caret-требование по умолчанию
serde = "^1.0.200" # явная запись того же самого
serde = "~1.0.200" # tilde: разрешён только патч, >=1.0.200, <1.1.0
serde = "=1.0.200" # точная версия, без свободы обновления
serde = "1" # эквивалентно ^1.0.0, >=1.0.0, <2.0.0
^1.2.3 разрешает 1.2.4, 1.9.0, но не 2.0.00.x.y caret трактует MINOR как breaking-границу: ^0.3.1 разрешает только 0.3.x, но не 0.4.0Cargo.toml описывает допустимые диапазоны версий, а Cargo.lock фиксирует точные версии, реально использованные при последней сборке — для всего графа зависимостей, включая транзитивные.
# фрагмент Cargo.lock (генерируется автоматически, не редактируется руками)
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5..."
Cargo.lock коммитится в репозиторий — гарантирует воспроизводимую сборку. Для библиотечных крейтов, публикуемых на crates.io, Cargo.lock обычно не коммитится: конечный потребитель библиотеки сам разрешает версии в контексте своего графа зависимостей, и зафиксированный lock-файл библиотеки на сборку потребителя не влияет.
crates.io — центральный публичный реестр пакетов Rust, аналог npmjs.com или PyPI. Публикация выполняется через CLI и требует обязательных метаданных в манифесте.
# Обязательные и рекомендуемые поля для публикации
[package]
name = "my-awesome-crate"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0" # обязательно: license ИЛИ license-file
description = "Краткое, но содержательное описание крейта"
repository = "https://github.com/user/my-awesome-crate"
homepage = "https://my-awesome-crate.dev"
documentation = "https://docs.rs/my-awesome-crate"
keywords = ["async", "http", "client"] # максимум 5
categories = ["web-programming"]
# Аутентификация — токен из crates.io/settings/tokens
cargo login
# Сухой прогон: что попадёт в архив, проверка компиляции пакета as-is
cargo publish --dry-run
# Публикация. Версии на crates.io НЕЛЬЗЯ перезаписать или удалить —
# только cargo yank помечает версию как нежелательную для новых проектов
cargo publish
license-file для нестандартных лицензий); без него публикация отклоняетсяunpublish в течение 72 часов возможен)Cargo.lock; уже существующие lock-файлы продолжают её использоватьcargo doc) для каждой опубликованной версииyank может сломать сборки тысяч зависимых проектов ниже по графу.
build.rs — опциональный Rust-файл в корне пакета (рядом с Cargo.toml), который Cargo компилирует и запускает перед основной сборкой. Используется для генерации кода, линковки нативных библиотек и передачи информации компилятору через специальные директивы вывода в stdout.
// build.rs
fn main() {
// Пересобирать, только если изменился нативный заголовок
println!("cargo:rerun-if-changed=vendor/mylib.h");
// Линковка C-библиотеки: путь и имя
println!("cargo:rustc-link-search=native=vendor/lib");
println!("cargo:rustc-link-lib=static=mylib");
// Прокинуть значение как переменную окружения времени компиляции,
// доступную в коде через env!("GIT_HASH")
let git_hash = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
// Условная компиляция: включить cfg-флаг для дальнейшего кода
println!("cargo:rustc-cfg=has_native_backend");
}
Типичные сценарии применения build scripts:
.proto-файлов в Rust-структуры через tonic-build/prost-build, результат кладётся в OUT_DIR и подключается макросом include!(concat!(env!("OUT_DIR"), "/generated.rs"))cc, указание путей поиска и имён библиотек для линкераcfgcargo:rerun-if-changed/cargo:rerun-if-env-changed — без них Cargo перестраховывается и перезапускает скрипт слишком часто, замедляя инкрементальную сборку. Также build-dependencies компилируются под хост-платформу отдельно от основных зависимостей, что важно учитывать при кросс-компиляции.
# Создание проекта
cargo new my-app # бинарный крейт (src/main.rs)
cargo new my-lib --lib # библиотечный крейт (src/lib.rs)
# Сборка и запуск
cargo build # debug-сборка
cargo build --release # release-сборка с оптимизациями
cargo run --bin api-server # запуск конкретного бинаря в workspace
# Работа с зависимостями и workspace
cargo add serde --features derive # добавить зависимость в Cargo.toml
cargo tree # дерево зависимостей — найти дубли версий
cargo test -p domain # тесты только для одного member workspace
cargo check --workspace # быстрая проверка типов без кодогенерации
# Аудит и качество
cargo fmt # форматирование по единому стилю
cargo clippy --all-targets # линтер, ловит идиоматические ошибки
cargo audit # проверка известных уязвимостей (cargo-audit)
В сравнении с Go, где go.mod/go.sum и стандартный toolchain покрывают похожий набор задач более минималистично, Cargo берёт на себя больше ролей сразу: сборщик, менеджер зависимостей, тест-раннер, линтер-обвязка и точка входа для публикации — всё через единый манифест и единый CLI.