Text handling is where Rust's ownership and borrowing rules become very practical. Rust has more than one string type because owned, growable text and borrowed text have different memory and lifetime requirements.
The two types you will use most often are String and &str.
String vs &str
| Type | Meaning | Typical use |
|---|---|---|
| String | Owned, growable UTF-8 text | Build, modify, store or own text |
| &str | Borrowed string slice | Read text without taking ownership |
String literals are slices
fn main() { let message: &str = "Hello, Rust!"; println!("{message}"); }A string literal is embedded in the program binary and is commonly used through a &str reference.
Create an owned String
fn main() { let first = String::from("Hello"); let second = "Rust".to_string(); println!("{first} {second}"); }Both create owned String values.
Why String is useful
Unlike a fixed string literal, a String can grow and change when declared mutable:
fn main() { let mut message = String::from("Hello"); message.push_str(", Rust"); message.push('!'); println!("{message}"); }Memory model of String
Stack Heap +----------------------+ +----------------------+ | ptr -----------------+----->| H e l l o , R u s t | | len | +----------------------+ | capacity | +----------------------+The String value tracks a pointer, length and capacity while owning the heap buffer.
Borrow String as &str
fn print_text(text: &str) { println!("{text}"); } fn main() { let owned = String::from("owned text"); print_text(&owned); print_text("literal text"); }This flexibility is why read-only function parameters often prefer &str instead of &String.
String slices
A slice references part of a string without owning it:
fn main() { let message = String::from("hello rust"); let hello = &message[0..5]; let rust = &message[6..10]; println!("{hello}"); println!("{rust}"); }message: h e l l o r u s t index: 0 1 2 3 4 5 6 7 8 9 └───────┘ └─────┘ &str &strRange shorthand
let text = String::from("hello rust"); let a = &text[0..5]; let b = &text[..5]; // same start: zero let c = &text[6..]; // from byte 6 to end let d = &text[..]; // complete string sliceImportant: Rust strings are UTF-8
Rust stores String and str text as UTF-8. A visible character does not necessarily occupy one byte.
Because of this, Rust does not support arbitrary integer indexing such as:
let text = String::from("hello"); // let first = text[0]; // not allowedInteger indexing would be ambiguous and could split a UTF-8 encoded character incorrectly.
Slicing must use UTF-8 character boundaries
Byte ranges used for a string slice must fall on valid character boundaries. An invalid boundary causes a runtime panic when that slicing expression executes.
Therefore, do not assume that character positions and byte offsets are always the same.
Iterate over Unicode scalar values with chars()
fn main() { let text = "Rust 🦀"; for ch in text.chars() { println!("{ch}"); } }Iterate over bytes
fn main() { for byte in "Rust".bytes() { println!("{byte}"); } }Choose chars() when you want Unicode scalar values and bytes() when byte-level processing is intentional.
Concatenate strings
Using push_str
let mut full = String::from("Rust"); full.push_str(" Language");Using format!
let language = "Rust"; let level = "Beginner"; let message = format!("{language} - {level}");format! is convenient because it creates a new String without making you manually manage concatenation ownership.
Using +
let first = String::from("Hello, "); let second = String::from("Rust"); let combined = first + &second; println!("{combined}"); println!("{second}");Notice that first is consumed by this operation, while second is borrowed.
Useful String methods
| Method | Purpose |
|---|---|
| len() | Length in bytes |
| is_empty() | Check whether length is zero |
| push() | Append one char |
| push_str() | Append a string slice |
| trim() | Return slice without leading/trailing whitespace |
| contains() | Check for a pattern |
| replace() | Create text with replacements |
| split() | Iterate over separated pieces |
| lines() | Iterate over lines |
| to_uppercase() | Create uppercase representation |
Parsing text into numbers
fn main() { let input = "42"; let number: i32 = input.parse().expect("valid integer"); println!("{}", number * 2); }For production code, prefer handling the returned Result instead of assuming input is always valid.
Practical example: first word
fn first_word(text: &str) -> &str { for (index, byte) in text.bytes().enumerate() { if byte == b' ' { return &text[..index]; } } text } fn main() { let sentence = String::from("Rust ownership matters"); let word = first_word(&sentence); println!("First word: {word}"); }The function returns a slice that remains tied to the input text rather than allocating a new String.
Why slices improve correctness
Suppose a function returned only an integer position for the end of the first word. That number has no direct relationship to the string after the function returns. A &str slice expresses the relationship in the type system: the result refers to the original text.
Array slices work similarly
fn main() { let values = [10, 20, 30, 40, 50]; let middle: &[i32] = &values[1..4]; println!("{:?}", middle); }Slices are not limited to strings. &[T] is a borrowed view into a contiguous sequence of values.
String ownership examples
Move
let a = String::from("Rust"); let b = a; // a is no longer usableBorrow
let a = String::from("Rust"); let b: &str = &a; println!("{a} {b}");Clone
let a = String::from("Rust"); let b = a.clone(); println!("{a} {b}");Common mistakes
- Trying to index a String with [0] .
- Assuming len() always means human-visible character count; for strings it reports bytes.
- Slicing at invalid UTF-8 boundaries.
- Accepting &String when &str would make an API more flexible.
- Cloning strings unnecessarily instead of borrowing.
Hands-on mini lab: command parser
cargo new string_lab cd string_labCreate a program that reads a command such as:
deploy productionThen:
- Use trim() .
- Split the line with split_whitespace() .
- Read the command and environment as &str values.
- Print a formatted confirmation using format! or println! .
- Handle missing values instead of indexing blindly.
Key points
- String owns mutable/growable UTF-8 text.
- &str borrows string data.
- String literals are commonly &str .
- Rust strings cannot be arbitrarily integer-indexed.
- String slice boundaries must be valid UTF-8 boundaries.
- Use chars() or bytes() depending on the representation you need.
- Prefer borrowing over unnecessary cloning.
Next chapter
Next, combine related values into your own types with Chapter 9: Rust Structs and Methods.
Official reference
Join the discussion
0 CommentsAsk questions, add context, or share what worked.
More from Melochip Editorial
Learn what Playwright testing is, why modern QA teams use it, how testers can get started, and the major types...
7 min read · Sep 19, 2026
Learn how Kubernetes Deployments and ReplicaSets work with practical YAML, scaling, self-healing, rolling upda...
7 min read · Sep 17, 2026
Learn Kubernetes security best practices with RBAC, Secrets, ServiceAccounts, Pod securityContext, NetworkPoli...
6 min read · Sep 17, 2026Related articles
Learn what Playwright testing is, why modern QA teams use it, how testers can get started, and the major types...
7 min read · Sep 19, 2026
Learn how Kubernetes Deployments and ReplicaSets work with practical YAML, scaling, self-healing, rolling upda...
7 min read · Sep 17, 2026
Learn Kubernetes security best practices with RBAC, Secrets, ServiceAccounts, Pod securityContext, NetworkPoli...
6 min read · Sep 17, 2026
Learn Kubernetes monitoring and logs with kubectl logs, events, metrics-server, top commands, Prometheus, Graf...
5 min read · Sep 17, 2026Stay in the loop. Get new Melochip articles
Receive Melochip article updates when new articles are available. Explicit opt-in only.
No comments yet. Start the discussion.