Frontend Labs
⋯

8. Slices and strings: fat pointers and UTF-8

8.1 Slices: a pointer and a length

A slice &[T] is a borrowed view into contiguous elements. It is a fat pointer: two words, (pointer, length). Unlike a Go slice it has no capacity, so it cannot grow and cannot write past its end.

fn sum(xs: &[i64]) -> i64 { xs.iter().sum() }   // accepts arrays, Vecs, and sub-slices

fn main() {
    let arr = [1i64, 2, 3, 4, 5];
    let v = vec![10i64, 20, 30];
    println!("{} {} {}", sum(&arr), sum(&v), sum(&arr[1..3])); // 15 60 5

    let mut buf = [0u8; 8];
    let (head, tail) = buf.split_at_mut(4);   // two non-overlapping &mut slices: allowed
    head[0] = 0xAA;
    tail[0] = 0xBB;
    println!("{buf:02x?}");
    println!("{}", std::mem::size_of::<&[u8]>()); // 16: pointer + length
}

Write functions that take &[T] instead of &Vec<T>, and &str instead of &String. They accept more kinds of input and give up nothing. Clippy warns about the other form (ptr_arg).

8.2 String vs &str

String &str
Owns its bytes? yes (heap) no (borrowed view)
Layout (ptr, len, cap) (ptr, len)
Growable? yes no
Literal String::from("x"), "x".to_string() "x", which is a &'static str stored in the binary
Guarantee always valid UTF-8 always valid UTF-8
fn greet(name: &str) -> String {
    format!("hello, {name}")       // format! allocates a new String
}

fn main() {
    let owned: String = String::from("Ada");
    let borrowed: &str = &owned;   // &String coerces to &str (Deref, Ch 18)
    let lit: &'static str = "Lovelace";
    let mut full = greet(borrowed);
    full.push(' ');
    full.push_str(lit);
    println!("{full}");
}

8.3 UTF-8: why s[0] does not compile

fn main() {
    let s = "héllo";                 // 'é' takes 2 bytes in UTF-8
    println!("{}", s.len());         // 6 BYTES, not 5 characters
    println!("{}", s.chars().count()); // 5 chars (Unicode scalar values)
    // let c = s[0];                 // error[E0277]: the type `str` cannot be indexed by `{integer}`
    println!("{}", &s[0..1]);        // "h": byte-range slicing is allowed...
    // println!("{}", &s[1..2]);     // ...but this PANICS: byte 2 is inside 'é'
    println!("{:?}", s.get(1..2));   // None: the non-panicking version
    for (i, c) in s.char_indices() { print!("{i}:{c} "); }
    println!();
    println!("{:?}", s.as_bytes());  // the raw bytes, when you need them
}

Compare Go §12: Go strings are UTF-8 by convention and can hold invalid bytes. Rust strings are UTF-8 by invariant. String::from_utf8(bytes) returns a Result, so invalid input is rejected at the boundary. For bytes that are not necessarily text, such as file contents, network payloads, or paths, use Vec<u8>/&[u8], OsString, or PathBuf.

Across the series: Unicode normalisation bugs, for example two different byte sequences that render as the same username, are covered in Security from Zero Ch 7. Valid UTF-8 does not make strings normalised. Handle that separately with the unicode-normalization crate.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36