How Does Ownership Work with Structs in Rust?

Struct ownership follows Rust's move semantics, transferring control of all fields to the new owner upon assignment.

The struct moves as a whole

You're building a configuration manager for a CLI tool. You define a Config struct holding the API key, the endpoint URL, and a timeout value. You load the config from a file, store it in a variable, and pass it to a function that initializes the network client. Immediately after, you try to log the configuration details to verify the setup. The compiler rejects the code. It highlights the logging line and complains that the config was moved. You haven't written a bug. You've hit the boundary of Rust's ownership model. Structs bundle data together, and they bundle ownership rules too. When you move a struct, you move the entire package. The compiler enforces this to ensure memory safety without a garbage collector.

Ownership flows through the container

A struct is a composite type. It groups named fields into a single unit. Ownership in Rust is linear. A value has one owner at a time. When ownership transfers, the previous owner loses access. This rule applies to structs exactly as it applies to primitive types, with one twist. Structs can contain other types. The ownership of the struct depends on the ownership of its fields. If a struct holds a String, moving the struct moves the String. If a struct holds an i32, the compiler might let you copy the struct instead. The key insight is that the struct is not a reference. It is the data. Assigning a struct variable to another variable transfers the data, not a pointer to the data.

Think of a struct as a sealed safe. The fields are the contents. Ownership is the key. If you hand the safe to someone else, you hand over the key. You can't open the safe after you've given it away. The safe moves as a whole. You can't hand over the safe but keep the combination for the inner drawer. The safe is a single object. Moving it moves everything inside. This prevents two people from claiming they own the safe and trying to destroy it at the same time.

Minimal example: the move in action

struct User {
    username: String,
    age: u8,
}

fn main() {
    let user1 = User {
        username: String::from("rustacean"),
        age: 25,
    };
    // user1 owns the User struct.
    // The String inside lives on the heap.
    // user1 holds the pointer, length, and capacity on the stack.

    let user2 = user1;
    // user1 is moved to user2.
    // The bits of user1 are copied to user2.
    // user1 is now invalid.

    // println!("{}", user1.username);
    // Error E0382: use of moved value `user1`.
    // The compiler prevents double-free by invalidating user1.
}

What happens under the hood

When user1 is created, Rust allocates memory for the User struct on the stack. The username field is a String, which consists of a pointer, length, and capacity. These three values live on the stack inside user1. The actual characters live on the heap. When user2 = user1 executes, Rust copies the stack bits from user1 to user2. The pointer, length, and capacity are duplicated. Now user2 points to the same heap string. If user1 remained valid, both variables would try to free the heap string when they go out of scope. Rust avoids this by marking user1 as moved. The borrow checker tracks this state. Any attempt to use user1 after the move triggers a compile error. The error happens at compile time, not runtime. This guarantees no double-free crashes.

The struct itself doesn't have special magic. The move behavior comes entirely from the fields. If a field implements the Drop trait, moving the struct ensures Drop runs exactly once. The compiler uses this to manage resources like file handles, network sockets, and memory allocations. Trust the borrow checker here. It tracks the lifecycle of every byte.

Realistic example: passing structs to functions

Structs are most useful when you pass them around. Functions often take ownership of structs to manage their lifecycle.

struct DatabaseConnection {
    host: String,
    port: u16,
    pool_size: usize,
}

/// Initializes the database pool and returns a status message.
fn connect(db: DatabaseConnection) -> String {
    // db is moved into this function.
    // The function owns the connection details.
    // When this function returns, db is dropped.
    format!("Connected to {} on port {}", db.host, db.port)
}

fn main() {
    let config = DatabaseConnection {
        host: String::from("db.example.com"),
        port: 5432,
        pool_size: 10,
    };

    let status = connect(config);
    // config is moved into connect.
    // config is no longer usable.

    // println!("Host: {}", config.host);
    // Error E0382: use of moved value `config`.

    // To use config again, you must borrow it or clone it.
    // Borrowing is cheap. Cloning copies the data.
    println!("Status: {}", status);
}

Convention aside: It's standard practice to add #[derive(Debug)] to structs so you can print them during development. Without it, println!("{:?}", config) fails. Also, cargo fmt formats structs consistently. Don't argue about brace placement or field alignment. Run the formatter and move on. Argue about field order and naming, not whitespace.

Pitfalls and compiler errors

The Copy trap

Structs with only Copy fields are automatically Copy. struct Point { x: i32, y: i32 } is Copy. let p2 = p1 copies, doesn't move. p1 remains valid. If you assume a move, you might write code that relies on p1 being invalidated, which won't happen. Check the trait bounds. i32, u8, bool, f64, char, and references implement Copy. String, Vec, Box, and custom structs without Copy do not. If you need a struct to be Copy, every field must be Copy. The compiler won't derive Copy for you if a field is missing it.

Partial moves

You can move a field out of a struct. let name = user.username; moves the String out. The struct user becomes partially moved. You can't use user anymore. The compiler rejects access to any field of a partially moved struct. This prevents using a struct with missing data.

struct Player {
    name: String,
    score: i32,
}

fn main() {
    let p = Player {
        name: String::from("Alice"),
        score: 0,
    };

    let name = p.name;
    // p.name is moved out.
    // p is now partially moved.

    // println!("{}", p.score);
    // Error E0382: use of partially moved value `p`.
    // You can't use a struct after moving one of its fields.
}

Moving out of references

You can't move data out of a reference. fn get_host(db: &DatabaseConnection) { let h = db.host; } fails. db is a reference. You can't take ownership of host through a reference. Error E0507: cannot move out of borrowed content. You must clone the field or return a reference to it. If you need the value, clone it. If you just need to read it, borrow it.

struct Config {
    api_key: String,
}

/// Returns a reference to the key.
fn peek_key(config: &Config) -> &str {
    // Returns a reference. No move happens.
    &config.api_key
}

/// Clones the key. Ownership transfers to the caller.
fn steal_key(config: &Config) -> String {
    // Cloning copies the heap data.
    // This is explicit and visible.
    config.api_key.clone()
}

Convention aside: The community prefers Rc::clone(&data) over data.clone() when using reference counting. Both compile, but data.clone() looks like a deep clone. The explicit form signals that you're cloning the wrapper, not the data. Apply the same clarity to structs. If you clone a struct, make sure readers know you're duplicating heap data, not just bumping a counter.

Decision: when to use this vs alternatives

Use a struct to group related data when you need to treat multiple values as a single unit. Use a struct with non-Copy fields when you want the compiler to enforce single ownership and prevent accidental data duplication. Use #[derive(Copy, Clone)] on a struct when every field is cheap to copy, like integers or booleans, and you want assignment to copy bits without moving. Use #[derive(Clone)] without Copy when the struct contains heap data but you occasionally need an explicit duplicate. Reach for references (&Struct) when you need to pass the struct to a function but keep ownership in the caller. Reach for Rc<Struct> when multiple owners need to share the same struct instance across different scopes.

Counter-intuitive but true: the more you clone, the more you hide performance costs. Borrow first. Clone only when you need independent mutation or the data must outlive the original scope.

Treat the struct as a vault. Move the vault, you move the gold. Don't try to pick the lock after you've handed it over.

Where to go next