When you need to split a string on the first occurrence of a delimiter, split_once is cleaner than anything you’d write by hand. Stable since Rust 1.52.
Parsing key=value pairs, HTTP headers, file paths — almost everywhere you split a string, you only care about the first separator. Before split_once, you’d reach for .find() plus index arithmetic:
The old way
1
2
3
4
5
6
7
8
| let s = "Content-Type: application/json; charset=utf-8";
let colon = s.find(':').unwrap();
let header = &s[..colon];
let value = s[colon + 1..].trim();
assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");
|
Works, but it’s four lines of noise. The index arithmetic is easy to get wrong, and .trim() is a separate step.
With split_once
1
2
3
4
5
6
| let s = "Content-Type: application/json; charset=utf-8";
let (header, value) = s.split_once(": ").unwrap();
assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");
|
One line. The delimiter is consumed, both sides are returned, and you pattern-match directly into named bindings.
Handling missing delimiters
split_once returns Option<(&str, &str)> — None if the delimiter isn’t found. This makes it composable with ? or if let:
1
2
3
4
5
6
7
| fn parse_env_var(s: &str) -> Option<(&str, &str)> {
s.split_once('=')
}
assert_eq!(parse_env_var("HOME=/root"), Some(("HOME", "/root")));
assert_eq!(parse_env_var("NOVALUE"), None);
assert_eq!(parse_env_var("KEY=a=b=c"), Some(("KEY", "a=b=c")));
|
Note the last case: split_once stops at the first =. The rest of the string — a=b=c — is kept intact in the second half. That’s usually exactly what you want.
rsplit_once — split from the right
When you need the last delimiter instead of the first, rsplit_once has you covered:
1
2
3
4
5
6
| let path = "/home/martin/projects/rustbites/content/posts/bite-044.md";
let (dir, filename) = path.rsplit_once('/').unwrap();
assert_eq!(dir, "/home/martin/projects/rustbites/content/posts");
assert_eq!(filename, "bite-044.md");
|
Multi-char delimiters work too
The delimiter can be any pattern — a char, a &str, or even a closure:
1
2
3
4
5
6
7
8
| let record = "alice::42::engineer";
let (name, rest) = record.split_once("::").unwrap();
let (age_str, role) = rest.split_once("::").unwrap();
assert_eq!(name, "alice");
assert_eq!(age_str, "42");
assert_eq!(role, "engineer");
|
Whenever you reach for .splitn(2, ...) just to grab two halves, replace it with split_once — the intent is clearer and the return type is more ergonomic.