How to Implement Methods on a Struct in Rust

Define an impl block for your struct and add functions with self as the first parameter to create methods.

When data needs behavior

You're building a game loop. You have a Player struct holding position, health, and inventory. You write a function take_damage(player: &Player, amount: u32). Then you write heal(player: &mut Player, amount: u32). Then move_player(player: &mut Player, dx: i32, dy: i32).

Suddenly your main function is a mess of take_damage(&player, 10) calls. The data and the logic are drifting apart. You want the Player to know how to take damage, not some random function floating in the global namespace. You want to write player.take_damage(10).

Rust lets you attach behavior directly to the data using impl blocks. The impl keyword binds functions to a type, turning standalone functions into methods. The syntax screams "this action belongs to this data."

The remote control analogy

Think of a struct as a piece of hardware. A Rectangle is just a box with dimensions. It doesn't do anything. An impl block is like the remote control for that hardware. It defines the buttons you can press.

The remote is tied to the device. You can't use a TV remote to program a microwave. In Rust, the impl block binds functions to the type, so you call rect.area() instead of area(&rect). The compiler enforces the connection. If you try to call area() on a Circle, the code won't compile. The method exists only on the type that owns the impl block.

Minimal example

Here is the basic pattern. You define the struct, then write an impl block with methods inside.

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

// The impl block attaches methods to Rectangle.
impl Rectangle {
    /// Calculate the area without modifying the rectangle.
    fn area(&self) -> u32 {
        // &self gives read-only access to the fields.
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle { width: 10, height: 20 };
    // Call the method using dot notation.
    println!("Area: {}", rect.area());
}

The impl keyword starts the block. Inside, you write functions. The first parameter is special. &self means "this method takes a reference to the instance calling it." When you write rect.area(), the compiler rewrites that behind the scenes to Rectangle::area(&rect). You get dot notation for free.

&self is a reference, so the method can read width and height but can't change them. The original rect stays valid. You can call area() as many times as you want. The method borrows the data, uses it, and returns. The caller keeps ownership.

Dot notation is syntax sugar. The compiler rewrites obj.method() to Type::method(&obj) behind the scenes.

Constructors and associated functions

Methods need an instance to work on. Sometimes you need to create the instance first. Constructors don't have an instance yet, so they can't take self. These are called associated functions.

struct Inventory {
    items: Vec<String>,
}

impl Inventory {
    /// Create a new empty inventory.
    /// This is an associated function, not a method.
    /// It doesn't take self because there's no instance yet.
    fn new() -> Self {
        Inventory { items: Vec::new() }
    }

    /// Add an item to the inventory.
    /// Requires &mut self because we're modifying the Vec.
    fn add(&mut self, item: String) {
        self.items.push(item);
    }

    /// Check if an item exists.
    /// Only needs &self since we're reading.
    fn has(&self, item: &str) -> bool {
        self.items.iter().any(|i| i == item)
    }
}

fn main() {
    // Call the associated function on the type, not an instance.
    let mut inv = Inventory::new();
    inv.add("Sword".to_string());
    println!("Has sword: {}", inv.has("Sword"));
}

Notice the return type Self. Self is a type alias for the type being implemented. Inside impl Inventory, Self is exactly the same as Inventory. Using Self saves typing and makes refactoring safer. If you rename the struct, Self updates automatically. The community convention is to use Self in return types for constructors and methods.

Name constructors new or from_x. Readers expect this pattern and will look for it first.

Visibility and multiple blocks

Methods can be public or private. pub fn exposes the method to other crates. fn keeps it internal to the current module. This is how you build a clean API. You expose area() but hide calculate_internal_cache().

pub struct Counter {
    count: u32,
}

impl Counter {
    /// Public constructor.
    pub fn new() -> Self {
        Counter { count: 0 }
    }

    /// Public method to increment.
    pub fn increment(&mut self) {
        self.count += 1;
    }

    /// Private helper. Other modules can't call this.
    fn validate(&self) -> bool {
        self.count < 1000
    }
}

You can split logic across multiple impl blocks for the same struct. The compiler merges them seamlessly. This is useful for organizing code by concern. You might put core logic in one block, trait implementations in another, and test helpers in a third.

impl Counter {
    pub fn increment(&mut self) {
        self.count += 1;
    }
}

// A second impl block for debug utilities.
impl Counter {
    pub fn debug_info(&self) -> String {
        format!("Count: {}", self.count)
    }
}

Split impl blocks to organize your code. The compiler merges them; your editor doesn't care.

Pitfalls and compiler errors

New Rust code often trips on self signatures. The compiler catches these mistakes early, but the errors can look confusing if you don't know what to look for.

Taking ownership by accident. If you write fn consume(self), the method takes ownership of the struct. You can't call it again. The value moves into the method and is dropped when the method returns.

fn main() {
    let rect = Rectangle { width: 10, height: 20 };
    rect.area();
    // If area took self, this line would fail with E0382.
    println!("Still valid: {}", rect.area());
}

If you accidentally use self instead of &self, the compiler rejects the second call with E0382 (use of moved value). The value is gone. Check your signature. You probably took ownership when you meant to borrow.

Mutable borrow conflicts. You can't call a &mut self method while a &self reference is active. The borrow checker prevents aliasing mutable data.

let mut inv = Inventory::new();
inv.add("Shield".to_string());

// This creates a shared reference.
let has_sword = inv.has("Sword");

// This tries to borrow mutably while has_sword is alive.
// Compiler error: E0502 (cannot borrow as mutable because it is also borrowed as immutable).
inv.add("Potion".to_string());

The error E0502 tells you exactly what's wrong. You have an immutable borrow active, and you're trying to mutate. Drop the immutable reference before mutating, or restructure the code.

Forgetting mut on the variable. If you call a &mut self method on a variable declared with let, the compiler rejects it.

let inv = Inventory::new();
// Compiler error: E0596 (cannot borrow as mutable, as it is not declared mutable).
inv.add("Key".to_string());

Add mut to the binding: let mut inv = Inventory::new();. The variable must be mutable to allow the method to mutate the data.

If the compiler rejects a method call, check the self signature. You likely promised ownership when you only had a reference.

Decision matrix

Choose the right signature based on what the method does. The borrow checker will enforce the contract you write.

Use &self when the method only reads fields and doesn't change state. This is the default for getters, calculations, and queries. Multiple &self methods can run concurrently on the same instance.

Use &mut self when the method modifies the struct's fields. Reserve this for mutations. The borrow checker will force you to handle exclusive access, preventing data races at compile time.

Use self (by value) when the method consumes the struct and transforms it into something else. This is rare and usually indicates a builder pattern or a state transition where the old value is dead. The caller loses the original instance.

Use an associated function (no self) when you need a constructor or a utility that doesn't require an existing instance. Name these new, from_x, or try_from_y. Call them on the type, not an instance.

Use multiple impl blocks when you want to separate logic by concern or isolate trait implementations from inherent methods. The compiler merges them seamlessly. This keeps large types readable.

Match the signature to the intent. The borrow checker enforces the contract you write.

Where to go next