A Rust String is guaranteed to be valid UTF-8 at every point in the type system — there is no way to construct an invalid one without deliberately bypassing the safety checks. Because a UTF-8 character can occupy 1 to 4 bytes, direct integer indexing (s[0]) is intentionally not supported, since it could split a multi-byte character and silently return nonsense.
Why you can't index a String by position
This is intentional, not a missing feature — the compiler refuses to compile code that could silently misbehave on non-ASCII text.
let s = String::from("héllo");
// let c = s[0]; // COMPILE ERROR: String cannot be indexed by integer
// use slicing on BYTE ranges instead, at your own risk of splitting a character:
let slice = &s[0..1]; // "h" — fine here, but risky in general on non-ASCIIIterating characters correctly with .chars()
.chars() gives an iterator over Unicode scalar values (essentially, characters), correctly decoding multi-byte UTF-8 — this is the right way to work character-by-character, mirroring the range-over-string behavior seen in Go's strings-and-runes topic.
let s = String::from("héllo");
for c in s.chars() {
print!("{} ", c); // h é l l o — each printed once, correctly decoded
}
println!("{}", s.chars().count()); // 5, the real character count
println!("{}", s.len()); // 6, the BYTE length (é is 2 bytes)Common string methods
String supports concatenation, splitting, trimming, and case conversion, mostly working on either String or &str interchangeably thanks to deref coercion.
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // + takes ownership of s1, borrows s2
let trimmed = " padded ".trim(); // "padded"
let upper = "hello".to_uppercase(); // "HELLO"
let parts: Vec<&str> = "a,b,c".split(',').collect(); // ["a", "b", "c"]