Rust String Types: Rust Difference Between str and String
Wed Aug 27th, 2025 β€” 57 days ago

Rust String Types: Rust Difference Between str and String

Rust’s primary UTF-8 string types are &str (borrowed, immutable) and String (owned, growable). Other string-like types exist for OS paths, FFI, and bytes.

TypeOwned/BorrowedUTF-8 GuaranteedNull-TerminatedTypical UseKey Conversions
&strBorrowedYesNoRead-only string slicess.to_string(), str::to_owned()
StringOwnedYesNoGrowable, heap-allocated stringsString::from(".."), my_string.as_str()
&[u8]BorrowedNoNoRaw bytesstd::str::from_utf8(bytes)
Vec<u8>OwnedNoNoOwned byte bufferString::from_utf8(vec)?, vec.as_slice()
Cow<'a, str>Borrowed/OwnedYesNoAPIs that may borrow or allocateCow::Borrowed, Cow::Owned(String)
CStrBorrowedNoYesBorrowed C strings (FFI)CStr::from_ptr(ptr), .to_str()?
CStringOwnedNoYesOwned C strings for FFICString::new("..")?, .as_c_str()
OsStrBorrowedPlatform-dep.NoOS strings (paths, args).to_string_lossy(), .to_os_string()
OsStringOwnedPlatform-dep.NoOwned OS strings.into_string() (may fail), .as_os_str()
PathBorrowedPlatform-dep.NoFilesystem paths.to_str(), .as_os_str()
PathBufOwnedPlatform-dep.NoOwned filesystem paths.as_path(), .into_os_string()

Problem

  • You need to know when to use &str vs String.
  • You need quick patterns for creating, converting, formatting, splitting, and multiline strings.

Solutions

Rust When to Use String vs str

  • Use &str to borrow read-only data. Cheap to pass around.
  • Use String when you own the data or need to modify/grow it.
fn greet(name: &str) {                // borrow: &str is ideal for params
    println!("Hello, {name}!");
}

let owned: String = String::from("Ada");
greet(&owned);                        // &String -> &str
greet("Turing");                      // &'static str

let mut s = String::from("Hi");
s.push_str(", there");                // need String to mutate

Create and Convert Between str and String

// &str -> String
let s1: String = "hello".to_string();
let s2: String = String::from("hello");

// String -> &str
let view: &str = s1.as_str();     // or &s1[..]

// Bytes <-> String (validate UTF-8)
let bytes: Vec<u8> = vec![97, 98, 99];               // a b c
let s_ok: String = String::from_utf8(bytes).unwrap();

let bad = vec![0xff, 0xfe];
let s_err = String::from_utf8(bad);                  // Err(FromUtf8Error)

Rust Strings (Basics)

let mut s = String::new();
s.push('A');
s.push_str("BC");
assert_eq!(s, "ABC");

let len_bytes = s.len();            // bytes, not chars
let first: char = s.chars().next().unwrap();

Rust Multiline String

let m = "line one
line two
line three";

let raw = r#"C:\path\no\escapes\needed "quotes" and \slashes"#;

Rust String Split

let csv = "a,b,c";
let parts: Vec<&str> = csv.split(',').collect();
assert_eq!(parts, ["a", "b", "c"]);

for tok in "  x  y ".split_whitespace() {
    println!("{tok}");
}

Rust String Format

let user = "Ada";
let n = 3;
let msg = format!("Hello, {user}. You have {n} messages.");
println!("{msg}");

let hex = format!("{:02X}", 255);   // "FF"

Things to Consider

  • String is UTF-8; length is in bytes, not chars.
  • Indexing String with [i] is not allowed. Use .chars(), .get(start..end), or iterators.
  • &str slices must be on UTF-8 char boundaries. Invalid slices panic.
  • Prefer &str in function parameters for flexibility. Accept both literals and String.
  • Use Cow<'a, str> when an API may return either a borrowed &str or an owned String.
  • Use OsStr/OsString for filesystem/OS text; not guaranteed UTF-8.
  • Use CStr/CString for FFI; NUL-terminated, no interior NULs.

Gotchas

Rust’s variety of string and path types can be confusing at first. Keep ownership and encoding in mind.

&str is a read-only view of UTF-8. It is cheap and ideal for parameters. String owns the data and can grow; use it when you need to build or mutate text. You can always borrow &str from a String via .as_str() or &s[..].

String is not indexable by integer because characters are variable-width in UTF-8. Slicing must respect character boundaries. Prefer iterating with .chars() or use .get(range) which returns Option<&str> and avoids panics.

OS and FFI strings are not regular UTF-8 text. Use OsStr/OsString for paths and command-line args; conversions to UTF-8 may fail or be lossy. Use CStr/CString at FFI boundaries and ensure no interior NUL bytes.

When splitting or formatting, remember that lengths and offsets are bytes. If you need character counts or grapheme clusters, use .chars() or a crate like unicode-segmentation. Allocate with String::with_capacity if you build big strings in a loop to reduce reallocations.


Sources


Further Investigation

  • CString/CStr for FFI, interior-NUL rules, and conversions.
  • OsStr/OsString and platform differences (Windows vs Unix).
  • unicode-segmentation crate for grapheme-aware slicing.
  • Cow<'a, str> patterns for zero-copy APIs.
  • Path/PathBuf usage and conversions.

TL;DR

Use &str to borrow read-only UTF-8. Use String to own/mutate text. Borrow &str from String when passing to functions.

fn takes_str(s: &str) { println!("{s}"); }

let mut owned = String::from("hello");
takes_str(&owned);          // &String -> &str
owned.push_str(" world");   // need String to mutate

let bytes = owned.into_bytes();              // Vec<u8>
let back  = String::from_utf8(bytes).unwrap();