Reading — step 1 of 5
Learn
Strings: String and &str
Ask any Rust programmer what confused them first and you'll hear this lesson's title. Rust has two string types, the split is the recurring beginner wall, and — once it clicks — it's also your first real glimpse of how Rust thinks about memory. Let's make it click early.
The two types
String— an owned, growable string. It's a buffer on the heap that belongs to your variable: you can push onto it, and it's freed when the variable goes out of scope.&str(say "string slice") — a borrowed view of string data living somewhere else. A pointer and a length; you can read through it, but it owns nothing.
let name: String = String::from("gopher"); // owned buffer
let lit: &str = "hello"; // literals are &str (baked into the binary)
let view: &str = &name; // borrow a view of the String
let owned: String = lit.to_string(); // copy borrowed data into a new owned buffer
The mental model that survives contact with the borrow checker: String is the house; &str is an address written on a card. You can hand out many cards; there's still one house; and whoever holds the house is responsible for it.
Which one do I use?
The rule of thumb the whole ecosystem follows:
- Function parameters: take
&str. Afn shout(s: &str)accepts both a&strand a&String(Rust auto-converts the reference) — maximally flexible, borrows instead of taking ownership. - Struct fields and return-values-you-created: use
String. If the data must outlive the call or be stored, it needs an owner.
fn shout(s: &str) -> String {
s.to_uppercase() // reads a borrow, returns a new owned String
}
That signature — borrow in, own out — is half the string functions you'll ever write.
Immutability, unless you own it
String literals and &str views are read-only. A String you own can grow — with mut:
let mut s = String::from("Hello");
s.push_str(", World"); // append a &str
s.push('!'); // append one char
s.len() // 13 — BYTES, not characters (UTF-8, same as Go)
And because Rust strings are UTF-8, indexing s[0] doesn't compile at all — a byte offset might land mid-character, and Rust refuses to guess. You iterate instead: s.chars() for characters, s.bytes() for raw bytes.
The toolbox
Methods live on the strings themselves, and both types share them (a String can do everything a &str can):
"go".to_uppercase() // "GO"
"GO".to_lowercase() // "go"
" hi \n".trim() // "hi" — your stdin friend
"seafood".contains("foo") // true
"golang".starts_with("go") // true
"a,b,c".split(',') // iterator over "a", "b", "c"
"ab".repeat(3) // "ababab"
format!("{}-{}", a, b) // combine anything into a new String
trim() deserves its highlight here too: read_line keeps the trailing \n, so nearly every parse of stdin input is line.trim().parse(). Forgetting the trim is the most common runtime panic in this course's early exercises.
Your exercise: Uppercase a Word
Read a word, print it uppercased. The one-liner is to_uppercase() — the lesson is in the types you touch on the way: read_line fills a String you own; trim() hands you a &str view into it; to_uppercase() reads that view and allocates a fresh String for the result. House → card → new house. Say the chain out loud once; when the borrow checker enters in chapter 4, you've already met its vocabulary.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…