Structs in Rust: Kinds, Derive & Ownership Guide

Max WellsMax WellsFounder of Rustify

TL;DR: A struct in Rust is a named grouping of fields, the primary way to define custom data types. Unlike C structs, Rust structs can have methods (via impl), implement traits, and participate fully in the ownership system. There are three kinds: named-field structs (most common), tuple structs, and unit structs. Rust has no class keyword, structs with impl blocks are the equivalent.


What Is a Struct in Rust?

A struct defines a named type by grouping fields together ; Rust's primary building block for custom data structures.

struct User {
    username: String,
    email: String,
    age: u32,
    active: bool,
}
 
fn main() {
    let user = User {
        username: String::from("alice"),
        email: String::from("[email protected]"),
        age: 30,
        active: true,
    };
 
    println!("{} ({})", user.username, user.age);
}

Fields are private to the module by default. Add pub to expose them, or (more commonly) expose behaviour through methods instead.


How Do You Add Methods to a Struct?

Methods are defined in impl blocks ; Rust's equivalent of class methods. &self borrows the instance read-only; &mut self borrows it mutably; self consumes it.

struct Rectangle {
    width: f64,
    height: f64,
}
 
impl Rectangle {
    // Associated function (constructor); called as Rectangle::new(...)
    pub fn new(width: f64, height: f64) -> Self {
        Self { width, height }
    }
 
    // Method; takes &self, read-only access
    pub fn area(&self) -> f64 {
        self.width * self.height
    }
 
    // Mutable method; takes &mut self
    pub fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
}
 
fn main() {
    let mut rect = Rectangle::new(10.0, 5.0);
    println!("Area: {}", rect.area()); // 50.0
    rect.scale(2.0);
    println!("Area after scaling: {}", rect.area()); // 200.0
}

Multiple impl blocks for the same struct are allowed, useful for organizing methods or implementing traits.


What Are the Three Kinds of Structs?

Named-field structs are most common. Tuple structs are useful for newtype wrappers. Unit structs carry no data and are used as marker types.

// 1. Named-field struct; most common
struct Point {
    x: f64,
    y: f64,
}
 
// 2. Tuple struct; fields accessed by index
struct Meters(f64);
struct Seconds(f64);
 
let distance = Meters(100.0);
let time = Seconds(9.58);
println!("{} m", distance.0); // 100.0
 
// Prevents mixing up types with same underlying type:
fn speed(d: Meters, t: Seconds) -> f64 { d.0 / t.0 }
// speed(time, distance); COMPILE ERROR: wrong order
 
// 3. Unit struct; no fields, often used as marker types
struct Marker;
struct AdminRole;

Tuple structs (also called newtypes) are a zero-cost way to add type safety, Meters and Seconds are both f64 wrappers but the compiler treats them as different types.


How Do You Derive Common Traits?

The #[derive] attribute auto-generates trait implementations for common behaviors, printing, cloning, comparing, and serializing.

#[derive(Debug, Clone, PartialEq)]
struct Config {
    host: String,
    port: u16,
    max_connections: u32,
}
 
fn main() {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        max_connections: 100,
    };
 
    println!("{:?}", config);           // Debug print
    let config2 = config.clone();       // Clone
    assert_eq!(config, config2);        // PartialEq comparison
}

Common derives:

DeriveWhat it provides
Debug{:?} formatting for debugging
Clone.clone() method for explicit duplication
CopyImplicit copy on assignment (only for small, stack types)
PartialEq== and != operators
HashUsable as HashMap key
serde::Serialize / DeserializeJSON/TOML/etc. serialization

How Do Structs Interact With Ownership?

Structs follow the same ownership rules as any other type, they are moved by default, unless they implement Copy. Fields can be partially moved out of a struct.

#[derive(Debug)]
struct Message {
    from: String,
    body: String,
}
 
fn process(msg: Message) {
    println!("From: {}", msg.from);
    // msg is consumed here; dropped at end of function
}
 
fn main() {
    let msg = Message {
        from: "Alice".to_string(),
        body: "Hello!".to_string(),
    };
 
    process(msg);
    // println!("{:?}", msg); // COMPILE ERROR: msg was moved
}

Use &Message to borrow (read), &mut Message to borrow mutably, or #[derive(Clone)] + .clone() to duplicate when you need to keep ownership.


Frequently Asked Questions

No. Rust uses structs + impl blocks instead. This separates data (struct) from behaviour (impl), and there is no inheritance. Reuse is achieved through trait implementations and composition.

..other_instance fills remaining fields from another instance of the same type:

let updated = User { email: "[email protected]".to_string(), ..existing_user };

Note: this moves fields from existing_user that are not Copy, so existing_user may be partially moved afterward.

Yes, but the reference must have a lifetime annotation:

struct Excerpt<'a> {
    text: &'a str,
}

For most cases, owning the data (using String instead of &str) is simpler and avoids lifetime annotations.

A struct is a product type, it has all of its fields at once. An enum is a sum type, it is one of its variants at a time. Use a struct when a value always has all the same fields; use an enum when a value can be in different states.


Sources


  • Enum: The complementary type, one of several variants
  • Trait: Behavior you can implement on a struct
  • Ownership: How struct values are moved and borrowed
  • Lifetime: Required when a struct holds references
  • Const Generics: Structs often use const generics for fixed-size arrays and numeric parameters
  • Module: Most real Rust code organizes related structs inside modules

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?