Structs in Rust
Everything you have written so far used types Rust handed you: numbers, bool, String, tuples, arrays. That works right up until your program is about something — a user, an order, a sensor reading, a crab in a tank — and you need a type that says so.
A struct is how you make one.
A struct groups related values into a single type, and gives every value a name.
That is the whole idea, and it is the foundation of essentially all Rust code you will ever read. This chapter covers defining structs, creating them, changing them, the shortcuts that save typing, and every error the compiler will throw at you along the way.
This chapter builds directly on Ownership and References and Borrowing. Structs do not add new ownership rules — they follow the ones you already know, which is exactly why they are introduced here.
The Problem: Related Data That Isn’t Related
Say you are tracking crabs in an aquarium. Each one has a name, a species, a weight, and whether it is currently moulting. With loose variables you would write:
let name = String::from("shelly");
let species = String::from("hermit");
let weight_grams = 92;
let is_molting = false;Four variables that describe one crab, and nothing in the code says so. Pass them to a function and it gets worse:
fn admit(name: String, species: String, weight_grams: u32, is_molting: bool) { }
// ^^^^^^^^^^^^^^^
// swap these two by accident and it still compiles perfectlyTwo String parameters next to each other is a bug waiting to happen. Nothing catches it.
Tuples Help a Little
A tuple at least keeps the values together:
fn main() {
let shelly = (String::from("shelly"), String::from("hermit"), 92, false);
println!("name: {}", shelly.0);
println!("species: {}", shelly.1);
println!("weight: {}g", shelly.2);
println!("molting: {}", shelly.3);
}Output:
name: shelly
species: hermit
weight: 92g
molting: falseBetter — one value instead of four. But now read shelly.2 and tell me what it means without scrolling up. Was weight index 2 or 3? Six months from now, in someone else’s code, .2 is a small puzzle every single time.
Positions are not meaning. A tuple is fine for something tiny and obvious like a coordinate pair, but the moment you cannot glance at .2 and know what it holds, you want a struct.
Defining a Struct
Use the struct keyword, a name, and a block listing each field as name: type:
struct Crab {
name: String,
species: String,
weight_grams: u32,
is_molting: bool,
}Three things to note:
- Struct names use
UpperCamelCaseby convention; field names usesnake_case. - Each field is
name: type, separated by commas. A trailing comma on the last field is normal Rust style. - This defines nothing that exists yet. It is a description of a shape, like a blueprint. No memory has been used.
Creating an Instance
To actually get a crab, write the struct name followed by a value for every field:
struct Crab {
name: String,
species: String,
weight_grams: u32,
is_molting: bool,
}
fn main() {
let shelly = Crab {
name: String::from("shelly"),
species: String::from("hermit"),
weight_grams: 92,
is_molting: false,
};
println!("{} is a {} crab", shelly.name, shelly.species);
println!("weight: {}g", shelly.weight_grams);
println!("molting: {}", shelly.is_molting);
}Output:
shelly is a hermit crab
weight: 92g
molting: falseshelly is an instance of Crab. You read its fields with a dot: shelly.name, shelly.weight_grams.
The definition is a shape. An instance is a thing with that shape.
The Order Does Not Matter
Because every field is named, you can write them in any order you like:
fn main() {
let pinchy = Crab {
weight_grams: 140,
species: String::from("lobster"),
is_molting: true,
name: String::from("pinchy"),
};
println!("{} the {} — {}g, molting: {}",
pinchy.name, pinchy.species, pinchy.weight_grams, pinchy.is_molting);
}Output:
pinchy the lobster — 140g, molting: trueThis is exactly what tuples cannot do, and it is the main reason structs exist.
But You Must Supply Every Field
Leave one out and the compiler stops you by name:
fn main() {
let shelly = Crab {
name: String::from("shelly"),
species: String::from("hermit"),
weight_grams: 92,
};
}error[E0063]: missing field `is_molting` in initializer of `Crab`
--> src/main.rs:9:18
|
9 | let shelly = Crab {
| ^^^^ missing `is_molting`
For more information about this error, try `rustc --explain E0063`.There is no such thing as a partly built struct in Rust. Every field has a value from the instant the instance exists, which is why you never have to wonder whether a field is “set yet”. (Defaults are possible later, via the Default trait.)
And the Field Has to Exist
Mistype a field name and you get a similarly direct error, complete with the list of what is actually available:
error[E0609]: no field `weight` on type `Crab`
--> src/main.rs:12:27
|
12 | println!("{}", shelly.weight);
| ^^^^^^ unknown field
|
= note: available fields are: `name`, `weight_grams`Changing a Struct: mut Applies to the Whole Instance
To change a field, the variable must be declared mut:
struct Crab {
name: String,
species: String,
weight_grams: u32,
is_molting: bool,
}
fn main() {
let mut shelly = Crab {
name: String::from("shelly"),
species: String::from("hermit"),
weight_grams: 92,
is_molting: false,
};
shelly.weight_grams = 97;
shelly.is_molting = true;
println!("{} the {}: {}g, molting: {}",
shelly.name, shelly.species, shelly.weight_grams, shelly.is_molting);
}Output:
shelly the hermit: 97g, molting: trueForget the mut and you get the error you would expect from Variables and Mutability, pointing at the exact line to change:
error[E0594]: cannot assign to `shelly.weight_grams`, as `shelly` is not declared as mutable
--> src/main.rs:12:5
|
12 | shelly.weight_grams = 97;
| ^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
7 | let mut shelly = Crab {
| +++You Cannot Make Just One Field Mutable
This is a question everyone asks, so let us answer it directly. You might hope to write:
struct Crab {
name: String,
mut weight_grams: u32,
}Rust does not even parse it:
error: expected identifier, found keyword `mut`
--> src/main.rs:3:5
|
1 | struct Crab {
| ---- while parsing this struct
2 | name: String,
3 | mut weight_grams: u32,
| ^^^ expected identifier, found keywordMutability in Rust belongs to the binding, not to the type. An instance is either mutable or it is not, all of it at once. That keeps the rule simple: if you hold a &mut Crab, you can change anything in it; if you hold a &Crab, you can change nothing.
All or nothing. It sounds restrictive and turns out to be the thing that makes borrowing predictable.
Functions That Build Structs
A function can return a struct like any other value. Here is one that admits a new crab with sensible starting values:
fn new_crab(name: String, species: String) -> Crab {
Crab {
name: name,
species: species,
weight_grams: 0,
is_molting: false,
}
}Notice the last expression in the function is the struct itself, with no semicolon — that is the expression-based return you already know.
Field Init Shorthand
Look at name: name and species: species. Writing the same word twice is noise, so Rust lets you drop the repetition whenever the parameter and the field share a name:
struct Crab {
name: String,
species: String,
weight_grams: u32,
is_molting: bool,
}
fn main() {
let bubbles = new_crab(String::from("bubbles"), String::from("fiddler"));
println!("{} the {} joined at {}g (molting: {})",
bubbles.name, bubbles.species, bubbles.weight_grams, bubbles.is_molting);
}
fn new_crab(name: String, species: String) -> Crab {
Crab {
name,
species,
weight_grams: 0,
is_molting: false,
}
}Output:
bubbles the fiddler joined at 0g (molting: false)name on its own means name: name. This is called field init shorthand, it is purely a typing saver, and idiomatic Rust uses it everywhere.
Struct Update Syntax: ..
Often a new instance differs from an existing one in only a field or two. Rather than retyping everything, list what changes and finish with .. and the instance to copy the rest from:
fn main() {
let shelly = Crab {
name: String::from("shelly"),
species: String::from("hermit"),
weight_grams: 92,
is_molting: false,
};
let nipper = Crab {
name: String::from("nipper"),
..shelly
};
println!("{} the {} — {}g, molting: {}",
nipper.name, nipper.species, nipper.weight_grams, nipper.is_molting);
}Output:
nipper the hermit — 92g, molting: falsenipper gets its own name and takes species, weight_grams and is_molting straight from shelly.
Two rules: ..instance must come last in the block, and it has no comma after it.
The Catch: .. Moves
This is the part that surprises people, and it is pure ownership — no new rule at all. Struct update syntax is an assignment, so every field it takes is either moved or copied, depending on whether that field’s type is Copy.
Field by field: one replaced, one moved, two copied.
So this fails:
let nipper = Crab {
name: String::from("nipper"),
..shelly
};
println!("{}", nipper.name);
println!("{}", shelly.species);error[E0382]: borrow of moved value: `shelly.species`
--> src/main.rs:22:20
|
16 | let nipper = Crab {
| __________________-
17 | | name: String::from("nipper"),
18 | | ..shelly
19 | | };
| |_____- value moved here
...
22 | println!("{}", shelly.species);
| ^^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `shelly.species` has type `String`, which does not implement the `Copy` traitBut — and this is the genuinely useful detail — only species actually moved. Rust tracks moves per field, so the rest of shelly is still perfectly usable:
let nipper = Crab {
name: String::from("nipper"),
..shelly
};
println!("new crab: {} the {} — {}g, molting: {}",
nipper.name, nipper.species, nipper.weight_grams, nipper.is_molting);
println!("shelly.name still works: {}", shelly.name);
println!("shelly.weight_grams still works: {}g", shelly.weight_grams);Output:
new crab: nipper the hermit — 92g, molting: false
shelly.name still works: shelly
shelly.weight_grams still works: 92gshelly.name survives because nipper supplied its own name and never took it. weight_grams and is_molting survive because u32 and bool are Copy, so they were duplicated rather than moved. Only species is gone — and with it, shelly as a whole value.
.. is not inheritance. It copies field values at that exact moment; the two instances have no ongoing connection. Change nipper.weight_grams afterwards and shelly is unaffected. If you want both instances to keep their own String data, use ..shelly.clone() or set the String fields explicitly.
Printing a Struct: #[derive(Debug)]
The first thing anyone tries is to print the whole struct. It does not work:
println!("{}", shelly);error[E0277]: `Crab` doesn't implement `std::fmt::Display`
--> src/main.rs:12:20
|
12 | println!("{}", shelly);
| ^^^^^^ `Crab` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Crab`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead{} is for things with one obvious human-readable form — a number, a string. Rust has no idea how you want a Crab displayed, so it refuses to guess. The note tells you the fix: use the debug placeholder {:?}, and give the struct a debug implementation:
#[derive(Debug)]
struct Crab {
name: String,
species: String,
weight_grams: u32,
is_molting: bool,
}
fn main() {
let shelly = Crab {
name: String::from("shelly"),
species: String::from("hermit"),
weight_grams: 92,
is_molting: false,
};
println!("{} the {} weighs {}g (molting: {})",
shelly.name, shelly.species, shelly.weight_grams, shelly.is_molting);
println!("{shelly:?}");
println!("{shelly:#?}");
}Output:
shelly the hermit weighs 92g (molting: false)
Crab { name: "shelly", species: "hermit", weight_grams: 92, is_molting: false }
Crab {
name: "shelly",
species: "hermit",
weight_grams: 92,
is_molting: false,
}#[derive(Debug)]is an attribute that asks the compiler to write the debug formatting for you.{:?}prints it on one line — good for quick checks.{:#?}pretty-prints it across several lines — good for anything with more than about three fields.
Add #[derive(Debug)] to essentially every struct you write. It costs nothing at runtime unless you actually print, and the one time you need to see inside a value at 2am you will be glad it is there.
How a Struct Sits in Memory
A struct is not a box around your data. The fields simply sit next to each other:
Exactly what four separate variables would look like — just given a shared name.
Two consequences worth internalising:
- A struct costs nothing. No header, no pointer indirection, no runtime type information.
shelly.weight_gramscompiles to a read at a fixed offset, the same machine code as reading a plain variable. Grouping values into a struct is free. - A struct owns its fields. When
shellygoes out of scope, Rust drops each field in turn, which frees both heap buffers. One value to think about instead of four.
Passing Structs to Functions
Structs follow the borrowing rules with no special cases. Take &Crab to read:
fn main() {
let shelly = Crab {
name: String::from("shelly"),
weight_grams: 92,
};
describe(&shelly);
describe(&shelly);
println!("main still owns {}", shelly.name);
}
fn describe(crab: &Crab) {
println!("{} weighs {}g", crab.name, crab.weight_grams);
}Output:
shelly weighs 92g
shelly weighs 92g
main still owns shellyAnd &mut Crab to change:
struct Crab {
name: String,
weight_grams: u32,
is_molting: bool,
}
fn main() {
let mut shelly = Crab {
name: String::from("shelly"),
weight_grams: 92,
is_molting: false,
};
feed(&mut shelly, 8);
feed(&mut shelly, 5);
println!("{} now weighs {}g (molting: {})",
shelly.name, shelly.weight_grams, shelly.is_molting);
}
fn feed(crab: &mut Crab, grams: u32) {
crab.weight_grams += grams;
crab.is_molting = crab.weight_grams > 100;
}Output:
shelly now weighs 105g (molting: true)Note there is no * anywhere. Rust dereferences automatically when you use . on a reference, so crab.weight_grams just works on a &mut Crab.
Borrow to read, borrow mutably to change, take by value only when you need to keep it. The same rule as every other type.
Tuple Structs
Sometimes a type needs a name but its fields do not. For that there are tuple structs — a struct name with types in parentheses and no field names:
struct TankId(u32);
struct CrabId(u32);
fn main() {
let tank = TankId(3);
let crab = CrabId(3);
println!("tank {} holds crab {}", tank.0, crab.0);
}Output:
tank 3 holds crab 3You reach the values by position — tank.0 — just like a tuple. So what is the point, when both are just a u32 underneath?
They are different types. That is the entire point:
fn main() {
let crab = CrabId(3);
drain(crab);
}
fn drain(tank: TankId) {
println!("draining tank {}", tank.0);
}error[E0308]: mismatched types
--> src/main.rs:7:11
|
7 | drain(crab);
| ----- ^^^^ expected `TankId`, found `CrabId`
| |
| arguments to this function are incorrectHad both been plain u32, that call would have compiled and drained the wrong thing. This pattern — wrapping one value to give it a distinct type — is called a newtype, and it is one of the cheapest safety wins in Rust.
Use tuple structs when:
- You are wrapping a single value to give it meaning:
TankId(u32),Grams(f64),Email(String). - The fields genuinely have no useful names:
Rgb(u8, u8, u8).
Use a normal struct as soon as a reader would have to ask what .1 means.
Unit-Like Structs
The smallest struct of all has no fields whatsoever:
struct Saltwater;
fn main() {
let water = Saltwater;
check(water);
}
fn check(_kind: Saltwater) {
println!("salinity check passed");
}Output:
salinity check passedIt holds no data and occupies no memory. It seems useless right now, and honestly it is until you reach traits — at which point a type that exists purely to be a type, and to have behaviour attached to it, becomes genuinely handy. They are called unit-like structs because they behave like (), the unit type.
Why Fields Own Their Data
You may have wondered why Crab uses String rather than the cheaper &str from the slices chapter. Try it:
struct Crab {
name: &str,
species: &str,
}error[E0106]: missing lifetime specifier
--> src/main.rs:2:11
|
2 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct Crab<'a> {
2 ~ name: &'a str,
|A reference must never outlive what it points at. If Crab holds a &str, the compiler has to know that whatever the crab’s name borrows from lives at least as long as the crab does — and there is no way to work that out from the definition alone. So it asks you to state it, with a lifetime parameter.
For now, the answer is simpler: let the struct own its data. Use String, not &str. Use Vec<T>, not &[T]. The struct then owns everything it holds and is valid for exactly as long as it exists — no lifetimes to reason about.
Reference fields are genuinely useful once you know lifetimes, mostly for types that borrow a buffer briefly, like a parser. Until then, owned fields are the right default, and the small cost of a String is almost never the thing that matters.
Putting It All Together
A small program using everything in this chapter: a named struct, a tuple struct, a struct containing other structs, field init shorthand, and both kinds of borrow.
struct Crab {
name: String,
species: String,
weight_grams: u32,
is_molting: bool,
}
struct TankId(u32);
struct Tank {
id: TankId,
label: String,
residents: Vec<Crab>,
}
fn main() {
let mut reef = Tank {
id: TankId(3),
label: String::from("coral reef"),
residents: Vec::new(),
};
admit(&mut reef, new_crab("shelly", "hermit", 92));
admit(&mut reef, new_crab("pinchy", "lobster", 140));
report(&reef);
}
fn new_crab(name: &str, species: &str, weight_grams: u32) -> Crab {
Crab {
name: name.to_string(),
species: species.to_string(),
weight_grams,
is_molting: weight_grams > 100,
}
}
fn admit(tank: &mut Tank, crab: Crab) {
println!("admitting {} to tank {}", crab.name, tank.id.0);
tank.residents.push(crab);
}
fn report(tank: &Tank) {
println!();
println!("--- tank {}: {} ({} residents) ---",
tank.id.0, tank.label, tank.residents.len());
for crab in &tank.residents {
let status = if crab.is_molting { "molting" } else { "healthy" };
println!(" {:<8} {:<8} {:>4}g {status}", crab.name, crab.species, crab.weight_grams);
}
}Output:
admitting shelly to tank 3
admitting pinchy to tank 3
--- tank 3: coral reef (2 residents) ---
shelly hermit 92g healthy
pinchy lobster 140g moltingFive things worth noticing:
Tankcontains aTankIdand aVec<Crab>. Structs nest freely, andtank.id.0reaches through both — the tuple struct’s field0inside theTank’s fieldid.new_crabtakes&strand calls.to_string(). That is the slices rule and the ownership rule working together: accept the flexible borrowed type, then allocate once when the struct actually needs to own it.weight_gramsuses field init shorthand, whileis_moltingis computed — the two styles mix freely in one block.admittakescrab: Crabby value because the tank is going to keep it. Thenpushmoves it into the vector. Borrowing would be wrong here; the crab genuinely changes hands.reporttakes&Tank, so it can read everything and owns nothing.for crab in &tank.residentsborrows each element rather than consuming the vector.
Error Cheat Sheet
Every error and warning structs will throw at you:
| Error | What it means | Usual fix |
|---|---|---|
| E0063 missing field in initializer | You left a field out when creating an instance | Supply every field — there are no partial structs |
E0609 no field x on type | Typo, or the field does not exist | Check the “available fields are” note in the error |
| E0594 cannot assign, not declared as mutable | You assigned to a field of a non-mut instance | let mut shelly = ... — mutability is all or nothing |
| E0382 borrow of moved value | ..instance moved a non-Copy field out | Use ..instance.clone(), or set that field explicitly |
E0277 doesn’t implement Display | You printed a struct with {} | Add #[derive(Debug)] and print with {:?} or {:#?} |
| E0106 missing lifetime specifier | A field holds a reference (&str, &[T]) | Own the data instead: String, Vec<T> |
| E0308 mismatched types | Two tuple structs wrapping the same inner type | Working as intended — that is the newtype safety net |
error: expected identifier, found keyword mut | You wrote mut inside a struct definition | Not a thing — put mut on the let instead |
| ⚠️ warning: fields are never read | The struct compiles but nothing reads those fields | Harmless while prototyping; #[derive(Debug)] alone does not silence it |
Every E#### can be explained in your terminal: run rustc --explain E0063 (or any other code) for a full write-up with examples, no internet needed.
Summary
| You write | What it is | Reach fields with |
|---|---|---|
struct Crab { name: String } | A named-field struct | crab.name |
struct TankId(u32); | A tuple struct | id.0 |
struct Saltwater; | A unit-like struct | nothing to reach |
Crab { name: ..., species: ... } | Creating an instance | — |
Crab { name, species } | Field init shorthand | — |
Crab { name: ..., ..shelly } | Struct update syntax (moves!) | — |
#[derive(Debug)] + {:?} | Printing a struct | — |
The mental shortcuts worth keeping:
- The definition is a shape; an instance is a thing with that shape.
- Every field is named, so nothing depends on remembering what position 2 meant.
mutbelongs to the binding, not the field. All of it or none of it...moves anything that isn’tCopy— it is an assignment, not inheritance.- Let structs own their data.
String, not&str, until you know lifetimes. - Structs are free. Fields sit side by side; field access is a fixed offset.
Frequently Asked Questions
What is a struct in Rust?
A struct is a custom type that groups several related values and gives each one a name. You write the definition once — listing each field name and its type — then create as many instances as you like. Unlike a tuple, where you reach values by position (.0, .1), every piece of a struct is reached by name, so the code says what it means without a comment.
How do you define and create a struct in Rust?
Define one with struct Name { field: Type, ... }, then create an instance with Name { field: value, ... }. You must supply every field — leaving one out is error E0063 — but the order you write them in does not have to match the definition, because each one is named.
How do you make a struct mutable in Rust?
Declare the variable with let mut. Rust has no way to mark a single field as mutable; the whole instance is mutable or it is not. Assigning to a field of a non-mut instance gives error E0594, and writing mut inside the struct definition is not valid syntax at all. Mutability belongs to the binding, not to the type.
What is struct update syntax in Rust?
It builds a new instance from an existing one: list only the fields you want to change, then finish with ..other_instance to take the rest. It must come last and takes no trailing comma. Watch out — it is an assignment, so any field that is not Copy is moved out of the original. A String field taken this way leaves the source instance partly unusable.
How do you print a struct in Rust?
Add #[derive(Debug)] above the definition and print with {:?}, or {:#?} for a pretty-printed version with one field per line. Printing with plain {} fails with error E0277 because structs do not implement Display — Rust will not guess how you want your type shown to a user.
What is the difference between a struct and a tuple in Rust?
A tuple groups values by position, and any two tuples with the same shape are the same type — so nothing stops you passing one where the other belongs. A struct names every value and creates a distinct type, which the compiler enforces even between two structs with identical fields. Use a tuple for something small and self-evident; use a struct as soon as .2 stops being obvious.
What is a tuple struct in Rust?
A tuple struct has types but no field names: struct TankId(u32);. You reach values by position (id.0). Their main use is the newtype pattern — wrapping one value to give it a distinct type, so a CrabId can never be passed where a TankId is expected. It costs nothing at runtime and catches a whole class of mix-ups at compile time.
Why should Rust struct fields own their data?
Because a field holding a reference needs a lifetime saying how long the borrowed data stays valid, and without one you get error E0106. Using owned types — String rather than &str, Vec<T> rather than &[T] — means the struct owns everything it holds and is valid for exactly as long as it exists. Reference fields become useful once you have learned lifetimes.
Does a Rust struct have any runtime overhead?
None. The fields sit next to each other in memory exactly as separate variables would, and field access compiles to a read at a fixed offset. There is no header, no indirection, and no runtime type information. Grouping values into a struct is free — the organisation exists entirely at compile time.
What’s Next?
You can now define your own types, build them, change them, print them, and pass them around under the same ownership rules as everything else.
Right now, though, the behaviour that belongs to a Crab is scattered across loose functions — describe, feed, new_crab — all taking a crab as their first argument. That is a strong hint. Next comes attaching those functions to the type itself as methods, so you write shelly.describe() instead, along with the impl block that makes it possible.
In the meantime, revisit Ownership with .. in mind — struct update syntax is just a move, and it should read as obvious now — or browse the rest of the Rust tutorial.