What is the difference between &T and T in function signatures

T takes ownership of a value, while &T borrows it, allowing the caller to retain access.

The contract in your signature

You write a function to process a user profile. You pass the profile in. The function runs. You try to print the profile again immediately after the call. The compiler rejects you with E0382 (use of moved value). You change the signature from fn process(user: User) to fn process(user: &User). The error vanishes. You can use the profile again.

Now you try to pass a string literal to a helper function that takes &String. The compiler complains about mismatched types. You change it to &str. It works.

This back-and-forth is the daily rhythm of Rust. The difference between T and &T in a function signature isn't just syntax. It's a contract about who holds the data, who can use it, and who cleans it up. Get the signature wrong, and the compiler blocks you. Get it right, and your code becomes self-documenting and safe by default.

Ownership versus borrowing

Rust's core rule is simple: every value has exactly one owner. The owner is responsible for cleaning the value up when its scope ends. This rule prevents memory leaks and dangling pointers without needing a garbage collector.

T in a function signature means the function takes ownership. The caller hands over the value. The caller can no longer use it. The function is now the sole owner. It can read the data, modify it, or drop it early. When the function returns, the value is dropped unless the function stores it somewhere else.

&T means the function borrows the value. The caller keeps ownership. The function gets a reference, which is like a pointer with a lifetime attached. The function can read the data. It cannot take ownership. It cannot drop the data. When the function returns, the reference disappears, but the data stays alive because the caller still owns it.

Think of T like handing someone your house keys. They can go inside, rearrange the furniture, or even demolish the house. You no longer have access. You gave up the keys. Think of &T like handing someone a photo of your house. They can look at the photo. They can't change the actual house. When they're done looking, they still have the photo, but you still have the keys and the house. The photo doesn't own the house.

Minimal example

/// Takes ownership of a String. The caller loses access.
fn takes_owned(name: String) {
    // `name` is the owner now.
    // We can modify it freely.
    name.push_str(" is awesome");
    println!("Inside: {}", name);
    // When this function ends, `name` is dropped.
    // The memory is freed.
}

/// Borrows a String. The caller keeps access.
fn takes_borrowed(name: &String) {
    // `name` is a reference.
    // We can read it, but we can't take ownership.
    // We can't modify it without `&mut`.
    println!("Inside: {}", name);
    // When this function ends, the reference is gone.
    // The underlying String is untouched.
}

fn main() {
    let user = String::from("Alice");

    // This works. We borrow `user`.
    takes_borrowed(&user);

    // We can still use `user` here.
    println!("User is still: {}", user);

    // This moves `user` into the function.
    takes_owned(user);

    // This would fail. `user` was moved.
    // println!("{}", user); // Error E0382
}

The signature is the law. The compiler enforces the contract you write. If you write T, you get a move. Period.

What happens under the hood

When you call a function with T, the compiler inserts a move operation. For a String, this means copying the pointer, length, and capacity from the caller's stack frame to the callee's stack frame. The heap data stays where it is. The original variable in the caller is marked as uninitialized. If you try to use it later, the compiler catches the use of uninitialized memory and emits E0382.

At runtime, the function executes. When the function returns, the compiler generates drop glue. The destructor for String runs, freeing the heap allocation. The memory is reclaimed.

When you call a function with &T, no move happens. The compiler creates a reference, which is just a pointer with a lifetime. The reference is passed to the function. The heap data is untouched. The caller's variable remains valid. When the function returns, the reference is dropped. Dropping a reference does nothing to the data. The data lives on.

E0382 isn't a bug. It's the compiler protecting you from using data that no longer exists.

Realistic scenario: validation versus archiving

Consider a system that manages users. You have a User struct. You need two functions: one to validate the user, and one to archive the user.

Validation should borrow. You only need to read the data. You don't need to store it. The caller might want to validate the same user multiple times.

Archiving should take ownership. The archive needs to keep the data. If the archive borrowed the user, the user would have to stay alive for the entire duration of the archive. That's impractical. The archive needs its own copy or the original data. Taking ownership enforces that the caller can't reuse the user after archiving, which prevents double-archiving or using stale data.

/// Represents a user in the system.
struct User {
    id: u32,
    name: String,
}

/// Validates a user by checking the name length.
/// Borrows the user because we don't need to own it.
fn validate_user(user: &User) -> bool {
    // We only read fields.
    // No need to take ownership.
    user.name.len() > 2
}

/// Archives a user by storing it in a global list.
/// Takes ownership because the archive needs to keep the data.
fn archive_user(user: User) {
    // We move `user` into the archive.
    // The caller can't use `user` anymore.
    // This prevents double-archiving or using stale data.
    println!("Archived user {}", user.name);
    // `user` is dropped here, along with the archive entry.
}

fn main() {
    let alice = User { id: 1, name: String::from("Alice") };

    // Validate first. We borrow.
    if validate_user(&alice) {
        println!("Alice is valid.");
    }

    // Now archive. We take ownership.
    archive_user(alice);

    // Alice is gone. Can't validate again.
    // validate_user(&alice); // Error E0382
}

Design your functions to take only what they need. Borrowing is the default. Ownership is the exception.

Pitfalls and compiler errors

The most common mistake is writing fn foo(x: T) when you meant &T. You move the value by accident. The caller gets E0382 on the next use. The fix is usually to change the signature to &T.

Another mistake is writing fn foo(x: &T) but trying to modify x. The compiler rejects this with E0596 (cannot borrow as mutable). You need &mut T to allow mutation.

A subtle trap is taking String when you only need to read text. If you write fn foo(x: &String), you can only accept &String. You can't pass a string literal like "hello". You can't pass a &str. You force the caller to allocate a String even if they have a literal. The community convention is to take &str instead. &str accepts &String, &str, and string literals. It's more flexible.

The same applies to collections. Prefer &[T] over &Vec<T>. Slices are more flexible. They accept Vec, arrays, and subslices. Containers are specific. Slices are general.

Prefer slices over containers. It makes your API usable by everyone.

Decision: when to use which

Use T when the function must store the value in a struct or a global collection. Use T when the value is cheap to move, like a u32 or a String where the caller is done with it anyway. Use T when you want to enforce that the caller cannot reuse the value, preventing double-processing. Use T when you are implementing a safe abstraction and need to manage the inner data yourself.

Use &T when the function only needs to read the data. Use &T when the caller needs to use the value after the function returns. Use &T when the value is expensive to clone and you don't need ownership. Use &T when you want to accept multiple types through trait objects or deref coercion, like &str accepting both String and literals.

Use &mut T when the function needs to modify the data in place. Use &mut T when you want to allow mutation without taking ownership. Use &mut T when you need exclusive access to prevent data races in concurrent code.

Default to borrowing. Take ownership only when you have a reason.

Where to go next