The wall between files
You are building a text adventure. Your main.rs has grown to four hundred lines. You have a Player struct, a Weapon enum, and a calculate_damage function all tangled together. Scrolling takes too long. You decide to move the weapon logic into its own file. You create weapon.rs. You paste the code. You try to compile. The compiler screams that Weapon is not found. You have hit the wall between files and modules.
Rust does not automatically merge files. Every file is isolated until you explicitly tell the compiler how they connect. This isolation is a feature. It prevents name collisions and keeps your project organized. But it means you have to learn two distinct tools: mod and use. One builds the structure. The other cleans up your typing.
Structure versus shortcuts
The mod keyword defines a module. A module is a namespace. It groups related code together and creates a boundary. When you write mod weapon, you are declaring a new box in your program's architecture. The compiler now expects code to live inside that box. If you provide the code inline, it goes there. If you use a semicolon, the compiler looks for a file named weapon.rs or a directory weapon/mod.rs.
The use keyword brings items into the current scope. It is a convenience shortcut. When you write use weapon::Weapon, you are telling the compiler: "In this function, whenever I type Weapon, look inside the weapon module for a public item with that name." It does not copy the code. It does not change visibility. It just creates a reference in the symbol table so you don't have to type the full path every time.
Think of mod as the architect drawing the floor plan. It defines where the kitchen is, where the bedroom is, and which doors are locked. use is you walking into the kitchen and deciding to call the fridge "fridge" instead of "kitchen::fridge". The fridge doesn't move. The kitchen doesn't change. You just saved yourself some typing.
mod organizes the codebase. use organizes your local scope. They solve different problems.
A minimal example
Here is the pattern in its simplest form. You define a module with a public function. You bring that function into scope. You call it.
/// Defines a module named `math_utils`.
/// This creates a namespace. Code inside is private by default.
mod math_utils {
/// Returns the square of a number.
/// The `pub` keyword makes this accessible outside the module.
pub fn square(x: i32) -> i32 {
x * x
}
}
/// Brings `square` into the current scope.
/// Now we can call `square()` instead of `math_utils::square()`.
use math_utils::square;
fn main() {
// Without `use`, this line would need `math_utils::square(5)`.
let result = square(5);
println!("5 squared is {}", result);
}
The mod block creates the namespace. The pub on the function opens the door. The use statement creates the shortcut. Remove any one of these, and the code breaks.
Don't treat use as a substitute for structure. Get the modules right first. Then clean up the noise.
How the compiler resolves paths
When you compile, Rust builds a module tree. The root is the crate. Every mod adds a branch. Every use adds a leaf to your current branch.
Consider this structure:
mod game {
pub mod player {
pub struct Hero {
pub name: String,
}
}
}
use game::player::Hero;
fn main() {
let hero = Hero { name: "Alice".to_string() };
}
The compiler sees mod game. It creates a game branch. Inside game, it sees mod player. It creates a player branch. Inside player, it sees pub struct Hero. It adds Hero to the player namespace.
The use game::player::Hero; statement walks that tree. It starts at the current scope, goes down to game, then to player, and finds Hero. Since Hero is pub, and player is pub, and game is pub, the path is valid. The compiler creates an alias. When you type Hero in main, the compiler resolves it to game::player::Hero.
If any part of the chain is missing pub, the walk stops. The compiler rejects the import. Visibility is transitive. You can only access what is explicitly exposed at every level.
use does not execute code. It is a compile-time operation. It affects zero runtime performance. It only changes how the compiler resolves names.
Real code across files
In a real project, modules usually live in separate files. This keeps files manageable. The syntax changes slightly. You declare the module with a semicolon, and the compiler looks for the file.
// main.rs
/// Declares the `utils` module.
/// The compiler looks for `utils.rs` in the same directory.
mod utils;
/// Imports `format_greeting` so we don't type `utils::format_greeting`.
use utils::format_greeting;
fn main() {
let msg = format_greeting("Alice");
println!("{}", msg);
}
// utils.rs
/// Formats a greeting string.
/// Must be `pub` to be accessible outside this module.
pub fn format_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
The mod utils; in main.rs is the bridge. It tells the compiler to include utils.rs in the build. Without that line, utils.rs is just a text file on disk. The compiler ignores it.
Community convention is to group use statements at the top of the file. It keeps imports visible and makes dependencies obvious. You can group multiple items with curly braces to save space:
use std::collections::{HashMap, HashSet};
This is equivalent to two separate use lines. It reduces vertical clutter. The compiler treats them identically.
Another convention is pub use. This lets a module re-export an item. It flattens the API surface. If you have a deep module tree, you can expose items at a higher level:
mod internal {
pub struct SecretData { /* ... */ }
}
pub use internal::SecretData;
Now users can write crate::SecretData instead of crate::internal::SecretData. This is how std::vec::Vec becomes available as std::Vec. You use pub use to design a clean public interface while keeping implementation details hidden.
Visibility is the gatekeeper. mod and use are just the map.
Pitfalls and compiler errors
New Rust code often trips over visibility and path resolution. The compiler catches these early, but the errors can be confusing if you don't know what to look for.
Private items block imports. You move a function to a new file. You add mod utils;. You try to use utils::helper;. The compiler rejects you with error[E0603]: module or function is private. The fix is to add pub to the item. Items are private by default. mod creates the box, but the door is locked unless you open it.
Missing mod declarations. You create weapon.rs. You try to use weapon::Weapon;. The compiler says error[E0432]: unresolved import. You forgot to declare the module. Add mod weapon; to the parent file. The compiler needs the declaration to know the file exists.
use does not change visibility. You can't use a private item from a parent module. The compiler will flag this immediately. use is a shortcut, not a permission slip. If the item isn't accessible, the import fails.
Shadowing. You can use a name that already exists. The new name shadows the old one. This is usually a mistake. The compiler warns about unused imports, but shadowing can hide bugs. Be careful with use in nested scopes.
Circular dependencies. You have mod a; and mod b;. a uses b, and b uses a. Rust handles this fine at the module level. The compiler builds the whole tree before checking dependencies. Circular imports are allowed as long as the types resolve. This is different from some other languages. You don't need to worry about import order.
Trust the borrow checker. It usually has a point. The module system is equally strict. Respect the boundaries.
When to use what
Choosing between mod, use, and full paths depends on your goal. Follow this decision matrix to keep your code clean and maintainable.
Use mod to define a namespace and organize code into logical groups. Use mod to declare external files so the compiler includes them in the build. Use mod when you want to encapsulate implementation details and expose only a public API. Use mod with a semicolon when the module lives in a separate file. Use mod with a block when the module is small and inline makes sense.
Use use to bring items into the current scope and reduce typing. Use use when you reference a type or function frequently and the full path is verbose. Use use to create aliases with as when names clash or the original name is confusing. Use use to group imports at the top of the file for readability. Use pub use to re-export items and flatten your public API.
Use full paths like crate::utils::helper when you want to be explicit about where a name comes from. Use full paths when two modules export the same name and you don't want to manage aliases. Use full paths in documentation examples to show the complete structure. Use full paths when the item is used only once and the path is short.
Structure first, shortcuts second. Get the modules right, then clean up the noise.