Rust structs
last modified October 25, 2026
In this article we show how to work with structs in Rust.
A struct is a custom data type that groups related values under one name. Each value is called a field. Structs are the main tool for modelling data in Rust; together with enums they replace the classes found in other languages.
Rust has three kinds of structs: structs with named fields, tuple structs with
unnamed fields, and unit structs with no fields at all. Behaviour is added in
separate impl blocks, which contain methods and associated
functions. By convention, struct names use CamelCase and field names use
snake_case.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
A simple struct
A struct is defined with the struct keyword, followed by its
name and a list of fields in curly braces. Each field has a name and a type.
struct User {
name: String,
occupation: String,
age: u32,
}
fn main() {
let u = User {
name: String::from("John Doe"),
occupation: String::from("gardener"),
age: 34,
};
println!("{} is a {}", u.name, u.occupation);
println!("{} is {} years old", u.name, u.age);
}
We define a User struct with three fields, create an instance of
it, and print its fields.
struct User {
name: String,
occupation: String,
age: u32,
}
The definition describes the shape of the data. The fields are separated by commas; a trailing comma after the last field is allowed and customary.
let u = User {
name: String::from("John Doe"),
occupation: String::from("gardener"),
age: 34,
};
An instance is created by writing the struct name and giving a value to every field. The fields can be listed in any order, but none of them may be left out.
println!("{} is a {}", u.name, u.occupation);
Fields are accessed with the dot operator.
$ cargo run -q John Doe is a gardener John Doe is 34 years old
Mutable instances
Like every binding in Rust, a struct instance is immutable by default.
struct Account {
owner: String,
balance: i64,
}
fn main() {
let mut acc = Account {
owner: String::from("Roger Roe"),
balance: 1200,
};
acc.balance += 300;
acc.owner.push_str(" Jr.");
println!("{}: {}", acc.owner, acc.balance);
}
To change a field, the whole instance must be declared with
mut.
let mut acc = Account {
owner: String::from("Roger Roe"),
balance: 1200,
};
Rust does not allow marking individual fields as mutable; mutability is a
property of the binding, not of the field. Without mut, both
assignments below would be compile-time errors.
acc.balance += 300;
acc.owner.push_str(" Jr.");
We add to the balance and append text to the owner's name.
$ cargo run -q Roger Roe Jr.: 1500
Field init shorthand
When a variable has the same name as a field, the field can be initialized with the variable name alone.
struct Book {
title: String,
author: String,
pages: u32,
}
fn new_book(title: &str, author: &str, pages: u32) -> Book {
Book {
title: title.to_string(),
author: author.to_string(),
pages,
}
}
fn main() {
let b = new_book("War and Peace", "Leo Tolstoy", 1225);
println!("{} by {}, {} pages", b.title, b.author, b.pages);
}
A function builds and returns a Book instance.
Book {
title: title.to_string(),
author: author.to_string(),
pages,
}
The pages line is a shorthand for pages: pages.
The other two fields need a conversion from &str to
String, so they are written out in full.
$ cargo run -q War and Peace by Leo Tolstoy, 1225 pages
Struct update syntax
The struct update syntax creates a new instance from an existing one, changing only some of the fields.
struct Config {
host: String,
port: u16,
verbose: bool,
retries: u32,
}
fn main() {
let base = Config {
host: String::from("localhost"),
port: 8080,
verbose: false,
retries: 3,
};
let dev = Config {
verbose: true,
retries: 0,
..base
};
println!("{}:{} verbose={} retries={}", dev.host, dev.port, dev.verbose, dev.retries);
println!("base port: {}", base.port);
}
We create a development configuration from a base configuration.
let dev = Config {
verbose: true,
retries: 0,
..base
};
The ..base part must come last. It takes the remaining fields,
host and port, from base.
println!("base port: {}", base.port);
The update syntax moves the fields it takes. The host string was
moved into dev, so base.host can no longer be used.
The port field is a u16, which is Copy;
it was copied and base.port is still valid.
$ cargo run -q localhost:8080 verbose=true retries=0 base port: 8080
Tuple structs
Tuple structs have a name but their fields have no names, only types.
struct Color(u8, u8, u8);
struct Point(i32, i32);
struct Meters(f64);
fn main() {
let red = Color(255, 0, 0);
let p = Point(3, -4);
println!("rgb({}, {}, {})", red.0, red.1, red.2);
let Point(x, y) = p;
println!("x: {x}, y: {y}");
let d = Meters(12.5);
let Meters(val) = d;
println!("distance: {val} m");
}
We define three tuple structs.
struct Color(u8, u8, u8); struct Point(i32, i32);
Although both structs consist of numbers, they are different types. A
function that takes a Point does not accept a
Color.
println!("rgb({}, {}, {})", red.0, red.1, red.2);
The fields are accessed by their position, as in a tuple.
let Point(x, y) = p;
A tuple struct can be destructured with a pattern that repeats the struct name.
$ cargo run -q rgb(255, 0, 0) x: 3, y: -4 distance: 12.5 m
The newtype pattern
A tuple struct with a single field is called a newtype. It wraps an existing type to give it a distinct meaning.
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(f64);
fn to_fahrenheit(c: Celsius) -> Fahrenheit {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
fn main() {
let temp = Celsius(21.5);
let f = to_fahrenheit(temp);
println!("{:?} = {:?}", temp, f);
println!("{:.1} °C is {:.1} °F", temp.0, f.0);
}
Both Celsius and Fahrenheit wrap an
f64, but the compiler treats them as unrelated types.
fn to_fahrenheit(c: Celsius) -> Fahrenheit {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
The signature documents the conversion and prevents mixing up the units:
passing a Fahrenheit value or a plain f64 does not
compile. The wrapper has no runtime cost; it occupies the same memory as the
inner value.
$ cargo run -q Celsius(21.5) = Fahrenheit(70.7) 21.5 °C is 70.7 °F
Unit structs
A unit struct has no fields. It is useful when we need a type to attach behaviour to, but have no data to store.
struct Logger;
impl Logger {
fn log(&self, msg: &str) {
println!("[log] {msg}");
}
}
fn main() {
let logger = Logger;
logger.log("application started");
logger.log("loading data");
println!("size of Logger: {}", std::mem::size_of::<Logger>());
}
The Logger struct has a method but no fields.
struct Logger;
A unit struct is declared with a semicolon instead of a body. An instance is
created by its name alone: let logger = Logger;.
println!("size of Logger: {}", std::mem::size_of::<Logger>());
A unit struct is a zero-sized type; it takes no memory at all.
$ cargo run -q [log] application started [log] loading data size of Logger: 0
Printing structs with Debug
Structs cannot be printed with {} or {:?} unless
they implement the Display or Debug trait. The
Debug trait can be derived automatically.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug)]
struct Line {
start: Point,
end: Point,
}
fn main() {
let ln = Line {
start: Point { x: 0, y: 0 },
end: Point { x: 5, y: 7 },
};
println!("{:?}", ln);
println!("{:#?}", ln);
let dx = ln.end.x - ln.start.x;
let dy = ln.end.y - ln.start.y;
println!("length: {:.2}", f64::from(dx * dx + dy * dy).sqrt());
}
We derive Debug for two structs, one nested in the other.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
The derive attribute generates the trait implementation. For a
derived Debug, all fields must implement Debug too;
that is why Point needs it to be printable as part of
Line.
println!("{:?}", ln);
println!("{:#?}", ln);
The {:?} specifier prints the struct on one line;
{:#?} pretty-prints it on multiple lines with indentation.
$ cargo run -q
Line { start: Point { x: 0, y: 0 }, end: Point { x: 5, y: 7 } }
Line {
start: Point {
x: 0,
y: 0,
},
end: Point {
x: 5,
y: 7,
},
}
length: 8.60
Methods
Methods are functions defined in an impl block of a type. Their
first parameter is self, the instance the method is called on.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let r1 = Rectangle { width: 30, height: 20 };
let r2 = Rectangle { width: 10, height: 15 };
println!("area: {}", r1.area());
println!("perimeter: {}", r1.perimeter());
println!("r1 can hold r2: {}", r1.can_hold(&r2));
println!("r2 can hold r1: {}", r2.can_hold(&r1));
}
The Rectangle struct gets three methods.
fn area(&self) -> u32 {
self.width * self.height
}
The &self parameter is short for self: &Self,
where Self is the type of the impl block. The method
borrows the instance immutably; it can read the fields but not change them.
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
A method can take additional parameters after self.
println!("r1 can hold r2: {}", r1.can_hold(&r2));
Methods are called with the dot syntax. Rust automatically borrows
r1 for the call; there is no need to write
(&r1).can_hold(&r2).
$ cargo run -q area: 600 perimeter: 100 r1 can hold r2: true r2 can hold r1: false
&mut self and self
The form of the self parameter determines what a method can do
with the instance.
struct Counter {
name: String,
count: u32,
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
fn reset(&mut self) {
self.count = 0;
}
fn into_name(self) -> String {
self.name
}
}
fn main() {
let mut c = Counter { name: String::from("visits"), count: 0 };
c.increment();
c.increment();
c.increment();
println!("{}: {}", c.name, c.count);
c.reset();
println!("{}: {}", c.name, c.count);
let name = c.into_name();
println!("name: {name}");
}
The example uses all three forms.
fn increment(&mut self) {
self.count += 1;
}
A method with &mut self borrows the instance mutably and can
modify its fields. It can be called only on a mutable binding.
fn into_name(self) -> String {
self.name
}
A method with self takes ownership of the instance. After
c.into_name(), the variable c is moved and cannot be
used anymore. Methods that convert a value into something else are by
convention named into_*.
$ cargo run -q visits: 3 visits: 0 name: visits
Associated functions and constants
Functions in an impl block that do not take self
are called associated functions. They are often used as
constructors.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
const MAX_SIDE: u32 = 1000;
fn new(width: u32, height: u32) -> Self {
Self {
width: width.min(Self::MAX_SIDE),
height: height.min(Self::MAX_SIDE),
}
}
fn square(side: u32) -> Self {
Self::new(side, side)
}
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let r = Rectangle::new(40, 25);
let s = Rectangle::square(12);
let big = Rectangle::new(5000, 30);
println!("{:?}", r);
println!("{:?}", s);
println!("{:?}", big);
println!("max side: {}", Rectangle::MAX_SIDE);
println!("area of square: {}", s.area());
}
The Rectangle struct has an associated constant, two associated
functions, and one method.
const MAX_SIDE: u32 = 1000;
An associated constant belongs to the type. It is accessed as
Self::MAX_SIDE inside the impl block and as
Rectangle::MAX_SIDE outside of it.
fn new(width: u32, height: u32) -> Self {
Self {
width: width.min(Self::MAX_SIDE),
height: height.min(Self::MAX_SIDE),
}
}
Rust has no special constructor syntax; new is an ordinary
associated function by convention. A constructor can validate or adjust the
data, here limiting the sides to the maximum.
let r = Rectangle::new(40, 25); let s = Rectangle::square(12);
Associated functions are called with the :: syntax on the type
name.
$ cargo run -q
Rectangle { width: 40, height: 25 }
Rectangle { width: 12, height: 12 }
Rectangle { width: 1000, height: 30 }
max side: 1000
area of square: 144
Implementing Display
To print a struct with {}, we implement the
std::fmt::Display trait. Unlike Debug, it cannot be
derived, because only the author knows how the value should look to users.
use std::fmt;
struct Point {
x: f64,
y: f64,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({:.1}, {:.1})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 2.5, y: -1.0 };
println!("p = {p}");
let s = p.to_string();
println!("length of string: {}", s.len());
}
We implement Display for a Point.
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({:.1}, {:.1})", self.x, self.y)
}
}
The fmt method writes the text representation into the
formatter with the write! macro.
let s = p.to_string();
Every type that implements Display also gets the
to_string method.
$ cargo run -q p = (2.5, -1.0) length of string: 11
Deriving common traits
Several standard traits can be derived to give a struct common behaviour.
#[derive(Debug, Clone, PartialEq, Default)]
struct Settings {
theme: String,
font_size: u32,
line_numbers: bool,
}
fn main() {
let s1 = Settings::default();
println!("{:?}", s1);
let mut s2 = s1.clone();
println!("equal: {}", s1 == s2);
s2.theme = String::from("dark");
s2.font_size = 14;
println!("equal: {}", s1 == s2);
println!("{:?}", s2);
}
The Settings struct derives four traits.
#[derive(Debug, Clone, PartialEq, Default)]
Clone adds the clone method that makes a deep copy,
PartialEq enables comparison with == and
!=, and Default provides a default
function that sets each field to its default value: an empty string,
0, and false.
let s1 = Settings::default();
The derived PartialEq compares the instances field by field.
$ cargo run -q
Settings { theme: "", font_size: 0, line_numbers: false }
equal: true
equal: false
Settings { theme: "dark", font_size: 14, line_numbers: false }
Destructuring structs
Patterns can take a struct apart into its fields, in let
statements, function parameters, and match arms.
struct Point {
x: i32,
y: i32,
}
fn describe(&Point { x, y }: &Point) -> String {
match (x, y) {
(0, 0) => String::from("origin"),
(0, _) => String::from("on the y axis"),
(_, 0) => String::from("on the x axis"),
_ => format!("at {x}, {y}"),
}
}
fn main() {
let p = Point { x: 4, y: 0 };
let Point { x, y } = p;
println!("x: {x}, y: {y}");
let Point { x: a, .. } = p;
println!("a: {a}");
let points = [Point { x: 0, y: 0 }, Point { x: 0, y: 5 }, p, Point { x: 2, y: 3 }];
for pt in &points {
println!("{}", describe(pt));
}
}
We destructure a Point in several ways.
let Point { x, y } = p;
The pattern binds the fields to variables of the same name.
let Point { x: a, .. } = p;
A field can be bound to a variable with a different name. The
.. part ignores the remaining fields.
fn describe(&Point { x, y }: &Point) -> String {
A pattern can be used directly in a function parameter. The parameter takes
a reference and the pattern copies the two i32 fields out of it.
The function then matches on the tuple of the coordinates.
$ cargo run -q x: 4, y: 0 a: 4 origin on the y axis on the x axis at 2, 3
A vector of structs
Structs are usually stored in collections and processed with iterators.
#[derive(Debug)]
struct Employee {
name: String,
dept: String,
salary: u32,
}
impl Employee {
fn new(name: &str, dept: &str, salary: u32) -> Self {
Self { name: name.into(), dept: dept.into(), salary }
}
}
fn main() {
let mut staff = vec![
Employee::new("Alice", "IT", 4200),
Employee::new("Bob", "Sales", 3100),
Employee::new("Carol", "IT", 5100),
Employee::new("Dave", "HR", 2900),
Employee::new("Eve", "Sales", 3600),
];
staff.sort_by_key(|e| std::cmp::Reverse(e.salary));
for e in &staff {
println!("{:<6} {:<6} {}", e.name, e.dept, e.salary);
}
let it_total: u32 = staff.iter().filter(|e| e.dept == "IT").map(|e| e.salary).sum();
println!("IT salaries: {it_total}");
if let Some(e) = staff.iter().find(|e| e.salary < 3000) {
println!("below 3000: {}", e.name);
}
}
We sort a vector of employees, compute a total, and search for an employee.
Self { name: name.into(), dept: dept.into(), salary }
The into method converts the &str arguments to
String; the target type is inferred from the field types.
staff.sort_by_key(|e| std::cmp::Reverse(e.salary));
The sort_by_key method sorts by a key extracted from each
element. Wrapping the key in Reverse sorts in descending order.
let it_total: u32 = staff.iter().filter(|e| e.dept == "IT").map(|e| e.salary).sum();
We filter the employees of a department and sum their salaries.
$ cargo run -q Carol IT 5100 Alice IT 4200 Eve Sales 3600 Bob Sales 3100 Dave HR 2900 IT salaries: 9300 below 3000: Dave
Source
The Rust Programming Language: Using Structs to Structure Related Data
In this article we have worked with structs in Rust. Enums, the other way to define custom types, are covered in Rust enums; shared behaviour is described in Rust traits.
Author
List all Rust tutorials.