Structs
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and structs are how you model real-world things by grouping related data together.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll define structs with named fields, create instances, and attach behaviour with methods in impl blocks.
What You'll Learn in This Lesson
- How to define a struct with named fields
- How to create and access struct instances
- Field init shorthand and mutating fields
- Adding methods with impl and &self
- Associated functions as constructors
- Deriving Debug to print structs
1️⃣ Defining and Using Structs
A struct declares a custom type with named, typed fields. You create an instance by setting every field, then read fields with dot notation. To change a field, the whole instance must be declared mut — Rust does not let you mark just one field mutable.
// A struct groups related data under named fields, each with a type.
struct User {
name: String,
age: u32,
active: bool,
}
fn main() {
// Create an instance by giving every field a value.
let user = User {
name: String::from("Ada"),
age: 36,
active: true,
};
// Access fields with a dot.
println!("{} is {}", user.name, user.age); // -> Ada is 36
println!("active? {}", user.active); // -> active? true
// To change a field, the whole instance must be mut.
let mut user2 = User { name: String::from("Bob"), age: 40, active: false };
user2.active = true;
println!("{} active? {}", user2.name, user2.active); // -> Bob active? true
}Each instance owns its data. Mutability applies to the whole instance, so user2 had to be mut before we could flip its active field.
2️⃣ Methods and Associated Functions
An impl block attaches behaviour to a struct. A method takes &self and is called with dot syntax on an instance. An associated function has no self and is called on the type with :: — perfect for constructors like new.
// Tuple structs and methods. impl blocks attach behaviour to a struct.
#[derive(Debug)] // lets us print with {:?}
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// An "associated function" (no self) — often used as a constructor.
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height } // field init shorthand: name == variable
}
// A METHOD takes &self to borrow the instance it's called on.
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let rect = Rectangle::new(4, 3); // call the associated function
println!("{:?}", rect); // -> Rectangle { width: 4, height: 3 }
println!("Area: {}", rect.area()); // -> Area: 12
println!("Square? {}", rect.is_square()); // -> Square? false
}The Rectangle { width, height } uses field init shorthand: when a variable has the same name as a field, you can write the field once. And #[derive(Debug)] let us print the whole struct with {:?}.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the struct and its method, then run it.
struct Circle {
radius: f64,
}
impl Circle {
// 1) Complete this method to return the area: 3.14159 * r * r.
// It must borrow self.
fn area(&self) -> f64 {
3.14159 * self.___ * self.radius // 👉 the field name
}
}
fn main() {
// 2) Build a Circle with radius 2.0.
let c = Circle { radius: ___ }; // 👉 the radius value 2.0
println!("Area: {:.2}", c.area()); // {:.2} rounds to 2 decimals
// ✅ Expected output:
// Area: 12.57
}Mini-Challenge: A Book Struct
Define a struct with a constructor and two methods. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: a Book struct with methods.
//
// 1. Define a struct Book with fields: title (String), pages (u32),
// and price (f64).
// 2. Add an impl block with:
// - new(title, pages, price) -> Book (a constructor)
// - summary(&self) -> String that returns
// format!("{} ({} pages)", self.title, self.pages)
// - is_long(&self) -> bool that is true when pages > 300
// 3. In main, build a Book "Rust 101", 320 pages, 39.99, then print the
// summary and whether it is long.
//
// ✅ Example output:
// Rust 101 (320 pages)
// Long book? true
fn main() {
// your code here
}Common Errors (and the fix)
- "missing field in initializer" — You must provide every field when creating a struct (unless you use struct update syntax ..other). Add the missing field.
- "cannot assign to field of immutable binding" — Declare the whole instance mut before changing any field.
- "`Rectangle` doesn't implement `Debug`" — Add #[derive(Debug)] above the struct to print it with {:?}.
- Calling a method like an associated function — Methods that take &self use dot syntax (r.area()), not Rectangle::area() without an instance.
Pro Tips
- 💡 Provide a new constructor: a fn new(...) associated function is the conventional way to build instances.
- 💡 Use field init shorthand: when a variable matches a field name, write the field once.
- 💡 Derive what you need: #[derive(Debug, Clone, PartialEq)] adds common capabilities for free.
📋 Quick Reference — Structs
| Goal | Rust Syntax |
|---|---|
| Define | struct P { x: i32 } |
| Create | let p = P { x: 1 }; |
| Read field | p.x |
| Method | fn area(&self) -> u32 |
| Constructor | P::new(1) |
| Printable | #[derive(Debug)] |
🎉 Lesson Complete!
- ✅ Structs group related data under named, typed fields
- ✅ Access fields with .; mutate them on a mut instance
- ✅ impl adds methods (&self) and associated functions (no self)
- ✅ #[derive(Debug)] makes a struct printable with {:?}
Practice quiz
What does a struct group together?
- Unrelated functions
- Only numbers
- Related data under named fields
- Threads
Answer: Related data under named fields. A struct groups related data, each value in a named, typed field.
How do you read a field of a struct instance?
- With dot notation, e.g. user.name
- With ::
- With []
- With get()
Answer: With dot notation, e.g. user.name. Fields are accessed with dot notation like user.name.
To change a single field, what must be true of the instance?
- The field must be pub
- It must be cloned
- It must be boxed
- The whole instance must be mut
Answer: The whole instance must be mut. Rust requires the whole instance to be mut; you cannot mark one field mutable.
What is the difference between a method and an associated function?
- Methods are faster
- A method takes self; an associated function does not
- Associated functions take self
- There is none
Answer: A method takes self; an associated function does not. Methods take self and use dot syntax; associated functions take no self and use ::.
How is an associated function like new typically called?
- Rectangle::new(4, 3)
- rect.new()
- new(Rectangle)
- Rectangle.new
Answer: Rectangle::new(4, 3). Associated functions are called on the type with ::, e.g. Rectangle::new(4, 3).
Why do methods usually take &self instead of self?
- It is shorter
- self is invalid
- &self borrows so the instance stays usable after the call
- It avoids the heap
Answer: &self borrows so the instance stays usable after the call. &self borrows the instance for reading without consuming it.
What does field init shorthand allow?
- Skipping fields
- Writing a field once when a variable shares its name
- Default values
- Renaming fields
Answer: Writing a field once when a variable shares its name. When a variable has the same name as a field, you write the field once.
What does #[derive(Debug)] enable?
- Cloning
- Equality
- Mutation
- Printing with {:?}
Answer: Printing with {:?}. Deriving Debug lets you print the struct with {:?} or {:#?}.
For impl Rectangle with area, what does Rectangle::new(4,3).area() return?
- 7
- 12
- 1
- false
Answer: 12. area multiplies width by height: 4 * 3 = 12.
What happens if you create a struct but omit a field?
- It defaults to 0
- It is set to None
- A compile error (missing field)
- Nothing
Answer: A compile error (missing field). Every field must be provided (unless using struct update syntax).
Continue this course
- Previous: Slices
- Next: Enums & Pattern Matching — Model values that can be one of several variants and match on them
- Quick reference: Rust cheat sheet › Structs & Enums
Frequently asked questions
What is the difference between a struct and a tuple?
Both group several values, but a struct gives each value a named field, while a tuple identifies values only by position. With a struct you write user.name and user.age, which is self-documenting; with a tuple you write user.0 and user.1, which is terser but easier to mix up. Use a struct when the values have distinct meanings worth naming, and a tuple for quick, throwaway groupings where the order is obvious.
What is the difference between a method and an associated function?
Both live in an impl block. A method takes self (usually &self) as its first parameter and is called on an instance with dot syntax, like rect.area(). An associated function does not take self and is called on the type with the :: syntax, like Rectangle::new(4, 3). Associated functions are commonly used as constructors. The key difference is that methods operate on a specific instance, while associated functions belong to the type as a whole.
Why do methods usually take &self instead of self?
Taking &self borrows the instance so the method can read its fields without taking ownership, which means you can keep using the instance after calling the method. Taking self by value would move the instance into the method and consume it. Use &self for read-only methods, &mut self when the method needs to modify the instance, and self only in the rarer case where the method should consume the instance.
What does #[derive(Debug)] do?
It automatically generates an implementation of the Debug trait for your struct, which lets you print it with the {:?} placeholder (or the pretty multi-line {:#?}). Without it, trying to print a struct with {:?} fails to compile because Rust does not know how to format your custom type. Deriving Debug is the quick, standard way to make a struct printable for debugging.