Structs
An overview of Rust structs – how to define, instantiate, and work with them, and where the real expressive power of the type system comes from.
Rust structs are the primary tool for grouping related pieces of data under one name. They sit at the heart of almost every Rust program, from small command-line tools to large networked services. If you have ever used records, classes (without inheritance), or data objects in other languages, structs will feel familiar – but Rust adds its own ownership and borrowing rules to the mix, which changes how you think about data layout and mutation.
The pages that follow walk through everything you need to use structs effectively. We start with the basic syntax, then move into methods, memory layout, trait derivation, interior mutability, and the type‑driven design patterns that make Rust code both safe and readable.
What a Struct Actually Is
A struct is a custom data type that bundles together a fixed set of named fields. Each field has a type, and every instance of the struct must provide values for all fields at creation time. That is the whole idea – but the implications run deep.
struct Point {
x: f64,
y: f64,
}
Here Point defines two fields, both f64. Once you have that definition, you can create a Point value like this:
let p = Point { x: 10.0, y: 20.5 };
println!("The point is at ({}, {})", p.x, p.y);
The fields are private to the module that defines the struct by default – you control what the outside world can access. That distinction becomes important as the codebase grows, and we will see how methods and visibility work together in the Method Syntax section.
Structs vs. classes:
Rust structs are often compared to classes in object‑oriented languages, but there is no inheritance. Instead, you compose behavior through trait implementations and generic type parameters.
A beginner can think of a struct as a custom shape for data – like a form with labeled boxes. You fill in each box with a value, and the compiler makes sure you never forget one and never put the wrong type in.
Why Structs Exist in Rust
The most obvious reason is organisation. Without structs, a function that needed a 2D point would take two separate f64 parameters; a function that needed a person’s record might take half a dozen arguments. That is error‑prone and hard to maintain. Structs give the group a name and make the data travel as one value.
Rust structs also unlock ownership guarantees. Because fields are stored inline (by default, directly inside the struct’s memory), moving a struct moves all its fields together, and borrowing a struct borrows the whole value. This consistency lets you reason about lifetimes without surprises.
Finally, structs are the foundation for Rust’s trait system. When you implement a trait on a struct, you attach behavior to that particular data shape. The standard library uses this pattern heavily – Clone, Debug, Iterator, Display, and many more all expect struct types.
Well‑formed data:
A struct that follows Rust’s ownership idioms and implements the right standard traits becomes a first‑class citizen of the ecosystem, easily printed, cloned, compared, or sent between threads.
Defining and Instantiating Structs
The page Defining and Instantiating Structs covers the full syntax. The shortest version is that you write struct Name { field: Type, ... } and then supply a value for every field when you build an instance. There are tuple structs and unit‑like structs too, each with their own use‑cases.
A common beginner mistake is forgetting that the variable holding the struct must be mut if you want to change any field – the struct itself is not automatically mutable just because a field is of a type that supports mutation.
Mutability is a variable property:
Marking the variable let mut makes the entire struct mutable. There is no per‑field mutability: you cannot have a mut x field inside an otherwise immutable struct instance. If you need controlled mutation, the Interior Mutability page explains the tools for that.
Methods and Associated Functions
A struct is just data until you add an impl block. Methods bring the data to life – they define what a struct can do. In Rust, a method is a function that takes self (or &self, &mut self) as its first parameter.
impl Point {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Calling p.distance_from_origin() feels familiar if you have used object notation elsewhere, but there is no hidden this – the receiver is explicit. Associated functions (those without self) are called with :: and serve as constructors or utility functions, like Point::new(0.0, 0.0).
The Method Syntax page explains the rules in detail, including automatic referencing, how self relates to ownership, and when to choose &self over &mut self.
Memory Layout and Generic Structs
Rust does not guarantee a specific field order in memory, but it does lay out the fields of a struct contiguously. That means the size of a struct is the sum of the sizes of its fields plus any padding the compiler inserts for alignment. Understanding this helps when you care about cache performance or when interfacing with C code (the #[repr(C)] attribute gives you a predictable layout).
Generics let you write a single struct definition that works with many different field types. For example, struct Pair<T, U> { first: T, second: U } gives you a pair of any two types. The compiler generates specialised versions for each concrete type you use, so there is no runtime cost.
The Struct Layout and Generic Structs page discusses these topics with examples and explains monomorphisation, alignment, and how to think about struct size.
Deriving Common Traits
Many standard traits can be implemented automatically by adding #[derive(...)] above a struct definition. The most commonly derived traits are Debug (for printing in a developer‑friendly format), Clone (for explicit duplication), Copy (for bitwise copy semantics when the struct is small and simple), and PartialEq / Eq (for comparison).
#[derive(Debug, Clone, PartialEq)]
struct Color {
r: u8,
g: u8,
b: u8,
}
With these derives, Color instantly gains the ability to be printed with {:?}, cloned, and compared with ==. The compiler checks that every field satisfies the trait bounds – if a field type doesn’t implement Copy, deriving Copy on the struct will fail with a clear error.
Copy is not always safe:
Deriving Copy changes how values move. A Copy type is implicitly duplicated instead of moved, which can be surprising when the struct is large or contains resources. Only derive Copy for small, plain‑data types where an implicit bitwise copy is cheap and free of side‑effects.
The Deriving Common Traits for Struct Types page shows the full list of derivable traits and explains the contracts each trait requires.
Interior Mutability – Mutating Through an Immutable Reference
By default, you cannot mutate data through a shared reference (&T). Interior mutability is a pattern that relaxes this rule safely by moving the check from compile time to runtime. The standard library provides Cell<T> for Copy types and RefCell<T> for types that need runtime borrow checking.
These are structs themselves, and they are used as field types inside otherwise immutable structs. For example:
use std::cell::RefCell;
struct Counter {
value: RefCell<u32>,
}
impl Counter {
fn increment(&self) {
*self.value.borrow_mut() += 1;
}
}
The &self method signature is immutable, yet the counter can change. The trade‑off is that RefCell panics at runtime if you violate the borrow rules (two mutable borrows at the same time, for instance), instead of refusing to compile.
The Interior Mutability page details when this is appropriate and how to combine it with Rc<T> for shared ownership.
Using the Type System to Express Data Structures
Rust’s type system does more than catch mistakes – it can actively shape your program’s logic. By choosing struct fields wisely, you can make invalid states impossible to represent, which eliminates entire categories of bugs.
For example, instead of using a bare bool for a connection state, you might define a struct that encodes the state directly in the type:
struct Connected;
struct Disconnected;
struct Connection<State> {
address: String,
state: std::marker::PhantomData<State>,
}
With phantom types, the compiler can enforce that only certain methods are available when the connection is in the right state. The Use the Type System to Express Data Structures page explores this and other techniques like using enums as struct fields to represent alternatives without ever creating a half‑valid object.
Builders and the Newtype Pattern
Two patterns emerge repeatedly in idiomatic Rust code.
The builder pattern solves the problem of constructing complex structs with many optional fields or configuration steps. Instead of a giant constructor with ten parameters, you create a separate builder struct that accumulates the settings and then produces the final object.
The newtype pattern wraps an existing type in a tuple struct with a single field to give it a distinct type. A struct Meters(f64); and a struct Feet(f64); are different types to the compiler, so you cannot accidentally mix them, even though they both contain f64 underneath. This gives you compile‑time unit safety at zero runtime cost.
These patterns are not language features – they are conventions that the community has found valuable. The pages Use Builders for Complex Types and Embrace the Newtype Pattern walk through examples and discuss when each pattern pays for itself.
Where This Fits in the Bigger Picture
Structs are the nouns of Rust programs. They form the backbone of Rust’s algebraic data types. Once you are comfortable designing structs, you will reach for them naturally when you see a group of values that travel together – and the compiler will guard you every step of the way.