ZetCode

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The Rust Reference: Structs

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.