Structs: Blueprints for memory
You are modeling a database record or a game entity. In Python, you reach for a dataclass or a dictionary. In JavaScript, you grab an object literal. Rust gives you struct, but it is not just a bag of properties. It is a layout guarantee. The compiler needs to know exactly how many bytes to reserve and where each piece of data lives. A struct in Rust is a blueprint for memory, not just a namespace.
Think of a struct as a custom mold for a 3D printer. You design the mold once, specifying exactly what slots it has and what shape each slot takes. Every time you print a part, the machine follows that mold. The mold does not hold the plastic; it defines the shape. In Rust, the struct definition is the mold. The variables you create are the printed parts. The compiler uses the mold to calculate memory offsets instantly. No reflection, no runtime shape guessing. The structure is fixed at compile time.
The three shapes
Rust provides three struct forms. Each serves a distinct purpose. You pick the shape based on how you want to access the data and whether the data carries meaning beyond its position.
/// Named struct: fields have names and types.
/// Most common form. Use this when field names carry meaning.
struct User {
name: String,
age: u32,
}
/// Tuple struct: fields are anonymous, accessed by index.
/// Use this when order matters but names are redundant, like coordinates.
struct Point(i32, i32);
/// Unit struct: no fields, just a type marker.
/// Use this to create a unique type for traits or state machines.
struct Marker;
Named structs are the workhorse. You access fields by name, which makes code readable and refactoring safe. Tuple structs behave like named tuples. You access fields by index, starting at zero. They are compact and useful when the data is inherently positional. Unit structs have no fields. They exist purely to create a unique type identity. You instantiate them without parentheses or braces.
Instantiation and access
Instantiating a named struct requires providing every field. The order does not matter, but the names must match. Tuple structs use parentheses and require arguments in the exact order defined.
fn main() {
// Instantiate named struct. Order is flexible; names are mandatory.
let user = User {
name: String::from("Alice"),
age: 25,
};
// Access fields with dot notation.
println!("Name: {}", user.name);
// Tuple struct instantiation. Order is strict.
let origin = Point(0, 0);
// Access tuple fields by index.
println!("X: {}", origin.0);
// Unit struct instantiation. No data, just the type.
let _marker = Marker;
}
If you have a variable with the same name as a field, you can use the field init shorthand. This is a community convention that appears in almost every Rust codebase. It reduces typing and eliminates copy-paste errors between the variable name and the field name.
fn main() {
let name = String::from("Alice");
let age = 25;
// Shorthand: `name` expands to `name: name`.
let user = User { name, age };
}
Use the shorthand whenever the variable name matches the field name. It signals to readers that you are binding the value directly, not transforming it.
Real-world patterns
Structs in production code rarely stand alone. You derive traits to get functionality for free, and you control visibility with pub. Fields are private by default. This is a hard rule. You must opt-in to exposure.
/// Derive common traits to get functionality for free.
/// Debug enables printing. Clone enables copying.
#[derive(Debug, Clone, PartialEq)]
struct Config {
pub host: String,
pub port: u16,
// Private field: only this module can access it.
api_key: String,
}
impl Config {
/// Constructor enforces validation logic.
fn new(host: &str, port: u16, key: &str) -> Self {
if port == 0 {
panic!("Port cannot be zero");
}
Config {
host: host.to_string(),
port,
api_key: key.to_string(),
}
}
}
The community convention for simple data transfer objects is pub fields. If you need invariants, make fields private and provide a constructor. Do not mix them arbitrarily. If a field is private, provide a getter or a method that exposes the data safely. Privacy is enforced at the module level, not the struct level. Code outside the module cannot touch private fields, even if it holds an instance of the struct.
Privacy is the default. You have to opt-in to exposure. This keeps your API surface small and your invariants safe.
The newtype wrapper
Tuple structs shine as wrappers. This pattern is called the newtype pattern. You wrap an existing type in a tuple struct to create a distinct type. This prevents mixing up values that have the same underlying representation but different meanings.
/// Newtype for user IDs.
struct UserId(u32);
/// Newtype for post IDs.
struct PostId(u32);
fn get_user(id: UserId) {
println!("Fetching user {}", id.0);
}
fn main() {
let user_id = UserId(42);
let post_id = PostId(42);
// This compiles.
get_user(user_id);
// This fails with E0308: mismatched types.
// get_user(post_id);
}
The compiler treats UserId and PostId as completely different types, even though both wrap u32. You cannot pass a PostId where a UserId is expected. This catches logical errors at compile time. You access the inner value via index, or you implement methods to expose it selectively.
Use the newtype pattern to attach traits to a type you do not own. If you need Serialize on a third-party type, you cannot implement it directly due to the orphan rule. You wrap it in a newtype and implement the trait on the wrapper.
Pitfalls and compiler errors
Structs enforce strict rules. Violating them produces clear errors.
If you forget a field during instantiation, the compiler rejects the code with E0063 (missing field). Rust structs are exhaustive. You cannot have a partial struct unless you use the update syntax with an existing instance.
struct Data {
a: i32,
b: i32,
}
fn main() {
// Error E0063: missing field `b`.
// let d = Data { a: 1 };
}
If you try to mutate a field on an immutable variable, the compiler gives E0596 (cannot assign to immutable variable). You need let mut to allow changes.
fn main() {
let mut d = Data { a: 1, b: 2 };
d.a = 10; // OK.
let immutable = Data { a: 1, b: 2 };
// Error E0596: cannot assign to `immutable.a`.
// immutable.a = 10;
}
Structs own their data. Moving a field moves the data out of the struct. If you move a field and then try to use the struct, you get E0382 (use of moved value). The struct is partially moved and cannot be used again.
fn main() {
let mut d = Data { a: 1, b: 2 };
let _val = d.a; // Moves `a` out.
// Error E0382: use of moved value `d`.
// println!("{}", d.b);
}
Update syntax moves data. If you use ..source, the fields are moved out of source. You cannot use source afterward unless it implements Copy. If you need both the original and the updated instance, clone first.
fn main() {
let original = Data { a: 1, b: 2 };
// Clone to preserve the original.
let updated = Data {
a: 10,
..original.clone()
};
// Both are usable.
println!("Original: {:?}", original);
println!("Updated: {:?}", updated);
}
Update syntax saves typing, but it consumes the source. Clone first if you need both.
Decision matrix
Use named-field structs when your data has semantic labels and you want self-documenting code. Use named-field structs when you need to initialize fields in any order or skip fields using update syntax. Use tuple structs when the data is a fixed sequence where position defines meaning, like coordinates or RGB values. Use tuple structs when you need a newtype wrapper to attach traits to an existing type without exposing the inner type. Use unit structs when you need a unique type identity but carry no data, such as implementing traits for a state or creating a marker for a type system.