Pattern Syntax
A comprehensive guide to pattern syntax in Rust covering literals, destructuring, guards, and bindings
Patterns are the shapes you write to test whether a value looks the way you expect. They appear in match arms, if let, while let, for loops, let statements, and function parameters—essentially anywhere Rust allows you to break apart data or make decisions based on structure. Unlike a simple equality check, a pattern can simultaneously inspect the form of a value and bind parts of it to local variables. This section details every piece of syntax you can use inside a pattern, why it exists, and how to avoid the mistakes most newcomers make.
Patterns beyond match:
While match is the most visible home for patterns, every let statement uses an irrefutable pattern: let x = 5; matches the value 5 against the pattern x. The same machinery operates behind for loops and function arguments.
Matching Literals, Named Variables, and Wildcards
The simplest patterns check for an exact value, bind to a new variable, or discard the value entirely.
You can write a literal directly in a pattern. If the scrutinized value equals that literal, the pattern matches. This is the closest analogue to a traditional switch on constants.
fn describe_digit(n: u8) {
match n {
0 => println!("zero"),
1 => println!("one"),
2..=9 => println!("small digit"),
_ => println!("not a digit"),
}
}
The pattern 0 is a literal that matches only the integer zero. The range 2..=9 covers several literals with a single syntax—more on ranges later. The arm _ is the wildcard pattern: it matches any value and does not bind it to a variable. Use _ when you need to satisfy exhaustiveness but have no use for the value itself.
Named variables inside patterns are irrefutable in isolation: a variable pattern matches anything and binds the matched value to that name. However, inside a match, if let, or while let, a new scope begins, and variable patterns shadow any outer variables with the same name.
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {y}");
Shadowing inside match arms:
The second arm Some(y) introduces a new y that binds to the inner value of the Some variant. It does not refer to the outer y = 10. The program prints Matched, y = 5 and then at the end: x = Some(5), y = 10. If you intended to compare against the outer y, you need a match guard—not a variable pattern.
This shadowing behavior is consistent with how every code block creates a fresh scope. Mentally treat each match arm as its own tiny scope; a variable introduced in the pattern exists only there.
Tuple and Struct Patterns
When you need to pull apart compound data, tuple and struct patterns let you access fields by position or by name.
A tuple pattern matches a tuple value and binds its elements to variables. The number of elements must match exactly.
fn midpoint((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> (f64, f64) {
((x1 + x2) / 2.0, (y1 + y2) / 2.0)
}
let point_pair = ((1.0, 2.0), (5.0, 6.0));
let mid = midpoint(point_pair.0, point_pair.1);
Here the function parameters use tuple patterns directly, destructuring the coordinates without extra let bindings. This is common for coordinates, RGB values, or any small, positional grouping.
Struct patterns mirror the structure of the type. You can use the field-name shorthand to avoid repetition.
struct Config {
port: u16,
debug: bool,
}
fn start_server(config: Config) {
let Config { port, debug } = config;
println!("Serving on port {port} (debug: {debug})");
}
The pattern Config { port, debug } creates local variables port and debug that hold the field values. You can also rename bindings with field: new_name or use literal values to test specific fields while binding others.
match &config {
Config { debug: true, port } => println!("Debug mode on port {port}"),
Config { debug: false, port: 0 } => println!("Disabled and no port set"),
_ => println!("Other configuration"),
}
Struct field matching is order-independent:
Unlike tuple patterns, the order of fields in a struct pattern does not matter; Rust matches by field name. This makes struct patterns robust when fields are reordered in the type definition.
Nested patterns work naturally. If a struct contains another struct or enum, you can drill into it in a single pattern.
enum Color {
Rgb(u8, u8, u8),
Hsv(u16, u8, u8),
}
struct Shape {
color: Color,
points: Vec<(i32, i32)>,
}
fn describe(shape: &Shape) {
match shape {
Shape { color: Color::Rgb(r, g, b), .. } => {
println!("RGB color ({r}, {g}, {b})")
}
Shape { color: Color::Hsv(h, s, v), points } if points.len() > 100 => {
println!("Large HSV shape with hue {h}°")
}
_ => println!("some shape"),
}
}
The first arm destructures a Shape, reaches into its color field to match Color::Rgb, and binds the three inner u8 values—all in one pattern.
Reference Patterns
Reference patterns strip a layer of indirection so you can work with the value behind a reference. They are most visible in iterator chains and when matching through borrows.
A reference pattern &x matches a reference and binds x to the referred-to value.
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();
The iterator yields &i32 references; the closure parameter &n immediately dereferences each one, so n is an i32. This is the same as writing |n| (*n) * 2 but keeps the dereference intent visible in the function signature.
When matching through a reference to an enum, you can combine reference patterns with inner destructuring. Historically this required the ref keyword, but since Rust 1.26 a feature called match ergonomics automatically inserts ref where needed, letting you write straightforward patterns against references.
fn describe_optional(value: &Option<String>) {
match value {
Some(s) => println!("Got string: {s}"),
None => println!("Nothing"),
}
}
Here value is a &Option<String>. The compiler sees that the scrutinee is a reference and that the pattern arms are not reference patterns, so it automatically dereferences and binds s by reference (s: &String). You can still use explicit & patterns or ref if you prefer, but the ergonomic form is idiomatic.
Match ergonomics keep patterns readable:
Before match ergonomics, you would have had to write &Some(ref s) => to achieve the same effect. The modern style reads more naturally while preserving full borrowing safety. If the code compiles, the borrows are correct.
The ref keyword itself still has a niche: when you want to bind a value inside a pattern by reference without the scrutinee being a reference, or when you need an explicit mutable borrow. ref mut x binds x as &mut T, just as &mut x would match through a &mut reference.
let mut data = (42, String::from("hello"));
match data {
(ref num, ref mut text) => {
*text = format!("{text} world");
println!("num: {num}");
}
}
Using ref avoids moving data while still allowing mutation of the String through a mutable borrow.
Matching Multiple Possibilities with |
The | operator lets a single arm accept multiple patterns, removing the need to duplicate logic.
fn is_monochrome(color: (u8, u8, u8)) -> bool {
match color {
(r, g, b) if r == g && g == b => true,
(0, 0, 0) | (255, 255, 255) => true,
_ => false,
}
}
Every alternative after | must be a complete pattern. You can combine | with match guards, but the guard applies to the entire arm—not to individual alternatives. The pattern 1 | 2 if some_condition means "match 1 or 2, and only if the guard holds."
`if` is not a pattern separator:
A common attempt is to write x if x > 5 | y if y > 10 =>—this does not work. The | only combines patterns; guards are attached to the whole arm after the pattern. For complex conditions, split arms or use the guard to test all needed conditions.
| works with any pattern type: literals, ranges, enum variants, structs, and nested patterns. For enum variants with data, each alternative must bind the same variables with the same types (or ignore them consistently) because the code following the arm assumes those bindings are available.
Pattern Guards
A pattern guard is the if clause that follows a pattern and imposes extra boolean conditions. It gives you the ability to filter matches based on values bound by the pattern or on external state, without sacrificing exhaustiveness checking.
let temp = 28;
match temp {
t if t < 0 => println!("freezing"),
t if t >= 0 && t < 20 => println!("cold"),
t if t >= 20 && t < 30 => println!("comfortable"),
t if t >= 30 => println!("hot"),
_ => unreachable!(),
}
The variable t is bound by the pattern, and the guard decides whether the arm fires. Without guards, you would need a separate if inside each arm, making exhaustiveness impossible to verify at compile time.
Guards are especially useful when you want to compare against an outer variable without introducing a shadowing binding.
let threshold = 10;
let number = Some(7);
match number {
Some(n) if n < threshold => println!("Below threshold"),
Some(n) => println!("Above threshold"),
None => println!("No value"),
}
If you had written Some(threshold) instead, that would introduce a new local threshold binding, not compare to the outer one. The guard removes that ambiguity.
Guard execution order matters:
Arms are tested top-to-bottom. The first pattern that matches, and whose guard evaluates to true, is selected. Even if a later arm's pattern also matches, it will never execute. Place the most specific guarded arms first.
The @ Binding
The @ operator lets you test a value against a subpattern while simultaneously binding the entire value to a variable. It is most useful when you need to use the whole matched value as well as its internal pieces.
enum Message {
Move { x: i32, y: i32 },
Write(String),
Quit,
}
fn process(msg: Message) {
match msg {
m @ Message::Move { x, y } if x < 0 && y < 0 => {
println!("Negative move: {:?}, adjusting", m);
// use m as the entire Message::Move
}
Message::Move { x, y } => println!("Moving to ({x}, {y})"),
Message::Write(text) => println!("Write: {text}"),
_ => {}
}
}
m @ Message::Move { .. } matches the variant and binds the whole Message to m. You can then pass m elsewhere or debug-print it without losing access to x and y.
@ works with range patterns as well: age @ 0..=17 matches if age falls in that range and binds age. It can also appear with | patterns: msg @ (Message::Quit | Message::Write(_)) matches either variant and binds the whole Message.
Ignoring Values with .. and _
Beyond the single-value wildcard _, Rust provides .. to ignore remaining parts of a struct, tuple, or enum variant. This tells the compiler and readers that you intentionally disregard those fields.
In a struct, .. skips any fields not explicitly mentioned.
struct Point3D {
x: f64,
y: f64,
z: f64,
}
let origin = Point3D { x: 0.0, y: 0.0, z: 0.0 };
match origin {
Point3D { x, .. } => println!("x is {x}"),
}
In a tuple, .. can appear at most once and skips any number of elements.
let triple = (1, 2, 3);
match triple {
(first, .., last) => println!("{first} and {last}"),
}
.. is also handy with slices:
let numbers = &[10, 20, 30, 40];
match numbers {
[first, rest @ .., last] => {
println!("first: {first}, middle: {rest:?}, last: {last}");
}
_ => {}
}
Here rest @ .. binds the middle slice to rest using the @ binding combined with ...
Use _ for a single ignored value, and .. for a range of ignored fields or elements. A struct pattern that uses .. must still mention at least one field, and the .. can be omitted if you list all fields explicitly.
The compiler watches ignored fields:
If a struct gains a new field, a pattern that used .. remains valid—the new field is silently ignored. Patterns that listed all fields, however, would break at compile time, which is often desirable to force you to handle the new data. Choose .. intentionally for partial matches where you explicitly accept future fields being ignored.
Summary
Pattern syntax in Rust is a single, composable system that shows up everywhere: from simple let bindings to deeply nested match expressions. Its building blocks—literals, variables, destructuring, reference handling, alternatives, guards, and bindings—fit together so that the shape of your code reflects the shape of your data. The compiler’s exhaustiveness and borrow checks then ensure that the patterns you write actually cover every possible case and never accidentally move or alias data.
If you walk away with one rule, it is this: when a match compiles, you are safe. The real power of patterns lies not in any individual syntax but in that guarantee.