Skip to content
Structs
step 1/7

Reading — step 1 of 7

Learn

~3 min readStructs, Enums, and Error Handling

Structs are how Rust groups related data into a named type. Chapter 5 of The Rust Programming Language book makes them the gateway from procedural code to type-driven design — and rightly so. Once you start defining your own types, the compiler can catch a whole new class of mistakes.

The three flavors of structs

1. Classic named-field structs

struct User {
    name: String,
    age: u32,
    active: bool,
}

let u = User {
    name: String::from("Ada"),
    age: 36,
    active: true,
};
println!("{}", u.name);    // Ada

Create with the field-init syntax. Access fields with .. Like a struct in C or a record in most languages.

2. Tuple structs — named tuples

When the field names don't add value:

struct Point(i32, i32);
struct Color(u8, u8, u8);

let origin = Point(0, 0);
let red = Color(255, 0, 0);
println!("{} {}", origin.0, origin.1);   // access by position

Useful when the type itself names the meaning (Point, Color) but individual fields don't need names. Different tuple structs are DIFFERENT TYPES — Point and Color aren't interchangeable even though both are (i32, i32)-shaped.

3. Unit structs — no fields

struct AlwaysReady;
let ready = AlwaysReady;

Useful for marker types and trait conformance without state.

Methods with impl blocks

Methods attach to a struct via impl:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    fn is_square(&self) -> bool {
        self.width == self.height
    }
    
    // Associated function (no self) — like a static constructor
    fn square(side: u32) -> Rectangle {
        Rectangle { width: side, height: side }
    }
}

let r = Rectangle { width: 3, height: 4 };
println!("{}", r.area());                  // 12
let sq = Rectangle::square(5);             // associated function syntax
println!("{}", sq.area());                  // 25

The first parameter is the receiver:

  • &self — borrow immutably (read-only access)
  • &mut self — borrow mutably (modify the struct)
  • self — take ownership (consume; rare)

Updating with the struct update syntax

let u1 = User { name: String::from("Ada"), age: 36, active: true };
let u2 = User {
    name: String::from("Linus"),
    ..u1                   // copy remaining fields from u1
};

The ..u1 says "and the other fields come from u1." Note: this MOVES non-Copy fields out of u1, so u1 may not be fully usable afterward.

Derive macros — automatic trait implementations

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 3, y: 4 };
let p2 = p1.clone();
println!("{:?}", p1);          // Debug — prints Point { x: 3, y: 4 }
println!("{}", p1 == p2);      // PartialEq — prints true

With one line, you get useful trait implementations the compiler generates from the field types. Almost every struct in real Rust code has at least #[derive(Debug)] for printing.

Common mistakes

  • Forgetting &self on methods — without a receiver, it's an associated function (called via Type::method), not an instance method.
  • Using pub struct without pub on fields — the struct is public but its fields are private. Often desired for encapsulation.
  • Cloning when borrowing would do — clone has cost. Use &self borrowing first, only clone when ownership is needed.
  • Forgetting to derive Debug — then println!("{:?}", ...) won't compile. Most types should derive Debug for diagnostics.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…