---
title: How Does Ownership Work with Structs in Rust?
summary: Struct ownership follows Rust's move semantics, transferring control of all fields to the new owner upon assignment.
lang: en
slug: how-does-ownership-work-with-structs-in-rust
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-does-ownership-work-with-structs-in-rust/
---

# How Does Ownership Work with Structs in Rust?



## 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

```rust
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.

```rust
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.

```rust
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.

```rust
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

- [How does ownership interact with pattern matching](/en/how-does-ownership-interact-with-pattern-matching/)
- [What is the difference between ownership and borrowing](/en/what-is-the-difference-between-ownership-and-borrowing/)
- [Ownership rules summary cheat sheet](/en/ownership-rules-summary-cheat-sheet/)