Skip to Content
Structs

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.


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 perfectly

Two 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:

src/main.rs
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: false

Better — 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:

src/main.rs
struct Crab { name: String, species: String, weight_grams: u32, is_molting: bool, }

Three things to note:

  • Struct names use UpperCamelCase by convention; field names use snake_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:

src/main.rs
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: false

shelly is an instance of Crab. You read its fields with a dot: shelly.name, shelly.weight_grams.

A Rust struct definition compared with an instance of it. On the left the blueprint, struct Crab, lists four field names with their types. In the middle an instance called shelly lists the same four field names with real values. Dotted arrows connect each field in the blueprint to the matching field in the instance. On the right, faded cards show that one blueprint can produce as many instances as you like.

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:

src/main.rs
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: true

This 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:

src/main.rs
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:

src/main.rs
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: true

Forget 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:

src/main.rs
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 keyword

Mutability 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:

src/main.rs
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:

src/main.rs
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:

src/main.rs
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: false

nipper 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.

What Rust struct update syntax moves and what it copies. The source instance shelly has four fields. Its name field is replaced by a fresh value so it is untouched. Its species field, a String, is moved out to the new instance and can no longer be used. Its weight_grams and is_molting fields are copied because u32 and bool are Copy types. Afterwards shelly.name, shelly.weight_grams and shelly.is_molting still work, but shelly.species is gone and shelly as a whole can no longer be used.

Field by field: one replaced, one moved, two copied.

So this fails:

src/main.rs
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` trait

But — 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:

src/main.rs
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: 92g

shelly.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:

src/main.rs
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:

src/main.rs
#[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:

How a Rust struct sits in memory. On the stack the instance shelly of type Crab holds four fields. The field name is a String made of a pointer, a length of 6 and a capacity of 6. The field species is another String with the same three parts. The field weight_grams holds the number 92 directly and is_molting holds false directly. The two String pointers lead to the heap, where the bytes of shelly and hermit are stored. The struct owns both Strings, so when shelly goes out of scope both heap buffers are freed with it.

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_grams compiles 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 shelly goes 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:

src/main.rs
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 shelly

And &mut Crab to change:

src/main.rs
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:

src/main.rs
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 3

You 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:

src/main.rs
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 incorrect

Had 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:

src/main.rs
struct Saltwater; fn main() { let water = Saltwater; check(water); } fn check(_kind: Saltwater) { println!("salinity check passed"); }

Output:

salinity check passed

It 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:

src/main.rs
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.

src/main.rs
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 molting

Five things worth noticing:

  • Tank contains a TankId and a Vec<Crab>. Structs nest freely, and tank.id.0 reaches through both — the tuple struct’s field 0 inside the Tank’s field id.
  • new_crab takes &str and 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_grams uses field init shorthand, while is_molting is computed — the two styles mix freely in one block.
  • admit takes crab: Crab by value because the tank is going to keep it. Then push moves it into the vector. Borrowing would be wrong here; the crab genuinely changes hands.
  • report takes &Tank, so it can read everything and owns nothing. for crab in &tank.residents borrows each element rather than consuming the vector.

Error Cheat Sheet

Every error and warning structs will throw at you:

ErrorWhat it meansUsual fix
E0063 missing field in initializerYou left a field out when creating an instanceSupply every field — there are no partial structs
E0609 no field x on typeTypo, or the field does not existCheck the “available fields are” note in the error
E0594 cannot assign, not declared as mutableYou assigned to a field of a non-mut instancelet mut shelly = ... — mutability is all or nothing
E0382 borrow of moved value..instance moved a non-Copy field outUse ..instance.clone(), or set that field explicitly
E0277 doesn’t implement DisplayYou printed a struct with {}Add #[derive(Debug)] and print with {:?} or {:#?}
E0106 missing lifetime specifierA field holds a reference (&str, &[T])Own the data instead: String, Vec<T>
E0308 mismatched typesTwo tuple structs wrapping the same inner typeWorking as intended — that is the newtype safety net
error: expected identifier, found keyword mutYou wrote mut inside a struct definitionNot a thing — put mut on the let instead
⚠️ warning: fields are never readThe struct compiles but nothing reads those fieldsHarmless 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 writeWhat it isReach fields with
struct Crab { name: String }A named-field structcrab.name
struct TankId(u32);A tuple structid.0
struct Saltwater;A unit-like structnothing 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.
  • mut belongs to the binding, not the field. All of it or none of it.
  • .. moves anything that isn’t Copy — 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.

Last updated on