Rust
8/27/2026
11 min read

Rust Slices: How to Borrow Part of an Array, Vec, or String

Rust Slices: How to Borrow Part of an Array, Vec, or String

A slice in Rust is a borrowed view into a run of contiguous memory. It stores two things: a pointer to the first element, and a count of how many elements follow. That is the whole idea. &[T] is a slice of any T, and &str is a slice of UTF-8 text. Neither owns the data it points at, which is why a slice cannot outlive whatever holds the real bytes.

This guide covers how to build slices from arrays, vectors and strings, why almost every function parameter should be a slice rather than a &Vec<T>, the safe alternatives to square-bracket indexing, and the standard library methods that make slices worth reaching for.

Creating a Slice

You create a slice by borrowing a range out of something contiguous:

fn main() {
    let numbers = [10, 20, 30, 40, 50];

    let all: &[i32] = &numbers;          // the whole array
    let middle: &[i32] = &numbers[1..4]; // [20, 30, 40]
    let head: &[i32] = &numbers[..2];    // [10, 20]
    let tail: &[i32] = &numbers[3..];    // [40, 50]

    println!("{middle:?}");
}

The range is half-open. 1..4 starts at index 1 and stops before index 4, so it holds three elements. Use 1..=3 when you want the end included.

The same syntax works on a Vec, because a Vec stores its elements contiguously on the heap:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let s: &[i32] = &v[0..3];
    println!("{s:?}"); // [1, 2, 3]
}

A slice is a fat pointer, meaning it is two machine words wide rather than one. You can see the length at any time:

let v = vec![1, 2, 3, 4, 5];
let s = &v[1..4];

assert_eq!(s.len(), 3);
assert!(!s.is_empty());
assert_eq!(s.first(), Some(&2));
assert_eq!(s.last(), Some(&4));

String Slices Behave Differently

&str is a slice of bytes that the compiler promises are valid UTF-8. That promise is what makes string slicing different from array slicing: the byte offsets you pass must land on character boundaries.

fn main() {
    let s = String::from("Hello, world!");
    let hello: &str = &s[0..5];
    println!("{hello}"); // Hello
}

Because the indices are byte offsets and not character counts, this panics at runtime:

let s = String::from("héllo");
let bad = &s[0..2]; // panics: byte index 2 is not a char boundary

The é occupies two bytes, so offset 2 lands in the middle of it. When you are working with text that is not plain ASCII, slice on boundaries the standard library gives you rather than on numbers you guessed:

fn main() {
    let s = "héllo";

    // Safe: ask for a checked slice.
    if let Some(part) = s.get(0..3) {
        println!("{part}"); // hé
    }

    // Safe: iterate by character.
    let first_three: String = s.chars().take(3).collect();
    println!("{first_three}"); // hél

    // Safe: find a real boundary.
    if let Some(idx) = s.find(&#x27;l&#x27;) {
        println!("{}", &s[idx..]); // llo
    }
}

Why Your Function Parameters Should Take Slices

This is the single most useful thing slices do, and it is the reason behind one of the most common compiler messages new Rust developers hit.

Take this function:

fn total(values: &Vec<i32>) -> i32 {
    values.iter().sum()
}

It works, but it only accepts a Vec. Callers holding an array, a boxed slice, or a slice of a larger vector cannot use it at all. Change the parameter to a slice and every one of them works:

fn total(values: &[i32]) -> i32 {
    values.iter().sum()
}

fn main() {
    let v = vec![1, 2, 3];
    let arr = [4, 5, 6];

    println!("{}", total(&v));        // Vec, via deref coercion
    println!("{}", total(&arr));      // array
    println!("{}", total(&v[1..]));   // slice of a Vec
}

&Vec<i32> coerces to &[i32] automatically, so taking the slice costs the caller nothing and loses nothing. The same rule applies to text: take &str, not &String.

// Accepts &str, String, and any slice of either.
fn shout(text: &str) -> String {
    text.to_uppercase()
}

When the compiler says "consider dereferencing the borrow to access the method in the slice", it is telling you that the method you want lives on [T] rather than on the reference you are holding, and that a deref will get you there. Changing the parameter type to &[T] usually removes the message entirely.

Indexing Without Panicking

Square brackets panic when the range is out of bounds. That is fine when the bounds are known at compile time and wrong when they come from user input, a file, or a network response.

let v = vec![1, 2, 3];
let boom = &v[0..10]; // panics: range end index 10 out of range

get returns an Option instead:

fn main() {
    let v = vec![1, 2, 3];

    match v.get(0..10) {
        Some(part) => println!("{part:?}"),
        None => println!("range does not fit"),
    }

    // A single element works the same way.
    assert_eq!(v.get(1), Some(&2));
    assert_eq!(v.get(99), None);
}

There is a mutable counterpart, get_mut, and both exist on str as well. Use get whenever the range is not something you control, and keep square brackets for the cases where an out-of-range index is a genuine bug you want to hear about loudly.

Mutable Slices and the Borrow Checker

A &mut [T] lets you change elements in place without touching the length:

fn double_all(values: &mut [i32]) {
    for n in values.iter_mut() {
        *n *= 2;
    }
}

fn main() {
    let mut numbers = [1, 2, 3, 4, 5];
    double_all(&mut numbers[1..4]);
    println!("{numbers:?}"); // [1, 4, 6, 8, 5]
}

The usual borrowing rules apply. You can hold many shared slices or exactly one mutable slice, never both at once. Two overlapping mutable slices of the same data are rejected at compile time rather than at run time.

When you genuinely need two mutable halves, the standard library has a method for it, because it can prove the halves do not overlap:

fn main() {
    let mut data = [1, 2, 3, 4, 5, 6];
    let (left, right) = data.split_at_mut(3);

    left[0] = 100;
    right[0] = 200;

    println!("{data:?}"); // [100, 2, 3, 200, 5, 6]
}

Note also that a mutable slice cannot push or remove elements. Length is fixed for the life of the slice. If you need to grow the collection, you need the owning Vec, not a slice of it.

Slice Methods Worth Knowing

Most of the useful work happens through methods on [T] rather than through manual indexing.

fn main() {
    let data = [5, 3, 9, 1, 7, 2];

    // Fixed-size groups. chunks_exact drops any short final group.
    for pair in data.chunks(2) {
        println!("{pair:?}"); // [5, 3] [9, 1] [7, 2]
    }

    // Overlapping runs.
    for w in data.windows(3) {
        println!("{w:?}"); // [5, 3, 9] [3, 9, 1] ...
    }

    // Split at an index, without copying.
    let (head, tail) = data.split_at(2);
    println!("{head:?} {tail:?}"); // [5, 3] [9, 1, 7, 2]
}

Sorting and searching operate on a mutable slice and never allocate:

fn main() {
    let mut data = [5, 3, 9, 1, 7, 2];

    data.sort();                      // [1, 2, 3, 5, 7, 9]
    data.sort_by_key(|n| *n % 3);     // sort by a derived key

    data.sort();
    assert_eq!(data.binary_search(&7), Ok(4));

    // partition_point finds the first index where a predicate stops holding.
    // The slice must already be ordered by that predicate.
    let first_big = data.partition_point(|&n| n < 5);
    assert_eq!(first_big, 3);
}

Bulk writes avoid element-by-element loops. fill sets every element, and copy_from_slice copies from another slice of exactly the same length:

fn main() {
    let mut buf = [0u8; 8];
    buf.fill(0xFF);

    let src = [1u8, 2, 3, 4, 5, 6, 7, 8];
    buf.copy_from_slice(&src); // panics if the lengths differ
    println!("{buf:?}");
}

copy_from_slice requires T: Copy. Use clone_from_slice for types that clone but do not copy.

Two more that come up constantly:

fn main() {
    let words = ["backend", "rust", "slices"];
    println!("{}", words.join(" "));   // "backend rust slices"

    let v: Vec<i32> = [1, 2, 3].to_vec(); // allocates an owned Vec
    println!("{v:?}");
}

to_vec is the escape hatch when you need ownership. It allocates, so call it deliberately rather than by habit.

Slice Patterns

Rust can destructure a slice directly, which is far more readable than a chain of index lookups:

fn describe(values: &[i32]) -> String {
    match values {
        [] => "empty".to_string(),
        [only] => format!("one element: {only}"),
        [first, .., last] => format!("starts {first}, ends {last}"),
    }
}

fn main() {
    println!("{}", describe(&[]));
    println!("{}", describe(&[42]));
    println!("{}", describe(&[1, 2, 3]));
}

The rest pattern .. can also be bound, which is how you take a head and a tail in one step:

fn main() {
    let list = vec![1, 2, 3, 4];

    if let [head, rest @ ..] = list.as_slice() {
        println!("head {head}, rest {rest:?}"); // head 1, rest [2, 3, 4]
    }
}

A pattern like let [x] = list.as_slice(); on its own will not compile, because a Vec of unknown length might not hold exactly one element. Use if let or match, as above, so the other lengths have somewhere to go.

Collections You Cannot Slice

Slicing needs contiguous memory. Arrays, Vec, String, str and VecDeque buffers qualify. Hash-based and linked collections do not.

CollectionSliceableWhat to do instead
[T; N], Vec<T>, String, strYesSlice directly with a range
VecDeque<T>Partlymake_contiguous returns a &mut [T]
HashMap<K, V>, BTreeMap<K, V>NoIterate, or collect into a Vec and slice that
HashSet<T>, BTreeSet<T>NoIterate, or collect into a Vec and slice that
LinkedList<T>NoIterate, or collect into a Vec

Collecting into a Vec first is a real allocation, so treat it as a deliberate choice rather than a reflex:

use std::collections::BTreeMap;

fn main() {
    let mut scores = BTreeMap::new();
    scores.insert("ada", 91);
    scores.insert("grace", 88);
    scores.insert("linus", 74);

    // BTreeMap iterates in key order, so no sort is needed here.
    let ranked: Vec<_> = scores.iter().collect();
    let top_two = &ranked[..2];

    println!("{top_two:?}");
}

With a HashMap the iteration order is unspecified, so sort before you slice or the result changes between runs.

Common Slice Errors and What They Mean

MessageCauseFix
range end index N out of range for slice of length MSquare-bracket range past the endUse get(..) and handle None
byte index N is not a char boundarySliced a &str in the middle of a multi-byte characterUse get, char_indices, or find
cannot borrow as mutable more than onceTwo overlapping &mut slicesUse split_at_mut or narrow the scopes
consider dereferencing the borrow to access the method in the sliceMethod lives on [T], not on the reference you holdChange the parameter to &[T]
source slice length does not match destinationcopy_from_slice with mismatched lengthsSlice both sides to the same length first
refutable pattern in local bindinglet [x] = v.as_slice();Use if let or match

How Slices Fit With the Rest of Rust

Slices are the borrowing half of a pattern that runs through the whole language. The owning type holds the data, the slice borrows a view of it, and conversions between the two are explicit. Our guide to Rust's From and Into traits covers the owning half of that story, including when to_vec is the right call and when a borrow is enough.

If you are putting these pieces to work on a server, our overview of Rust for backend development walks through where the language pays off, and our roundup of the top Rust frameworks compares the web frameworks built on top of it.

Frequently Asked Questions

What Is the Difference Between an Array, a Vec, and a Slice?

An array has a fixed length known at compile time and usually lives on the stack. A Vec owns a growable heap buffer. A slice owns nothing: it borrows a run of elements out of either one, and stores only a pointer and a length.

Why Should a Function Take &[T] Instead of &Vec<T>?

&Vec<T> accepts only vectors. &[T] accepts vectors, arrays, boxed slices and sub-slices, because &Vec<T> coerces to &[T] automatically. The slice version is strictly more useful and costs the caller nothing.

How Do I Slice a String Safely in Rust?

Use get with a range and handle the None case, or work through chars, char_indices or find so the offsets you use are real character boundaries. Square brackets on a &str panic when an index lands inside a multi-byte character.

Can a Slice Change the Length of the Collection?

No. A &mut [T] can change the values in place but cannot push or remove elements. Length changes need the owning Vec or String.

How Do I Get Two Mutable Slices of the Same Vec?

Call split_at_mut, which returns a pair of non-overlapping mutable slices. The borrow checker rejects two overlapping mutable slices because it cannot prove they are disjoint, and split_at_mut is the standard library's way of proving it.

Summary

A slice is a pointer and a length that borrows part of something contiguous. Build one with a range on an array, a Vec or a String, and remember that ranges over text are byte offsets that must land on character boundaries.

Take &[T] and &str in your function signatures rather than &Vec<T> and &String, because the slice version accepts everything the owned version does and more. Use get when the range comes from outside your program, square brackets when an out-of-range index is a bug you want to surface, and split_at_mut when you need two mutable views at once. Reach for the standard library methods, chunks, windows, binary_search, partition_point, fill and copy_from_slice, before you write an indexing loop of your own. When a collection is not contiguous, iterate it or collect into a Vec first, and treat that allocation as a decision rather than a habit.

Tags

Enjoyed this article?

Subscribe to our newsletter for more backend engineering insights and tutorials.