Basic Syntax: Variables, Types, and Pattern Matching
In the previous post, we discussed why Rust is worth learning and how to run your first Hello World. This post dives directly into syntax—variables, types, control flow, functions, and pattern matching. The core goal is simple: enable you to read and write Rust code.
If you have a Go background, none of these concepts will be unfamiliar. I’ll provide comparisons at key points to help map your existing knowledge to Rust.
Variable Declaration
Rust’s variable declaration is straightforward, with a core principle: immutable by default, add mut when mutability is needed:
| |
Go/Rust Comparison:
| Go | Rust | |
|---|---|---|
| Immutable binding | const x = 5 (package-level) or x := 5 (function-level) | let x = 5 |
| Mutable binding | x := 5 | let mut x = 5 |
| Variable shadowing | x := 5; x := 10 (syntax error) | let x = 5; let x = 10 (OK) |
| Compile-time constant | const PI = 3.14 | const PI: f64 = 3.14 |
Go doesn’t have truly immutable bindings—const only applies to package-level constants, and := declarations in functions are mutable by default. Rust defaults to immutable, requiring explicit mut annotation for modification.
Variable shadowing is a unique Rust feature. Go doesn’t allow shadowing with the same variable name, but Rust allows creating new bindings with the same name via new let, even changing types. This is practical in data transformation scenarios—parse a string to a number, then perform calculations.
const in both Go and Rust are compile-time constants, but Rust’s const requires explicit type annotation.
Basic Data Types
Rust’s type system retains traditional classifications while introducing stricter type constraints:
| Type Category | Rust Types | Description |
|---|---|---|
| Signed Integers | i8, i16, i32, i64, i128, isize | isize = platform-dependent signed integer |
| Unsigned Integers | u8, u16, u32, u64, u128, usize | usize = platform-dependent unsigned integer |
| Floating Point | f32, f64 | IEEE 754 standard |
| Boolean | bool | true / false |
| Character | char | Unicode scalar value (4 bytes) |
| String | &str, String | String slice / heap-allocated string |
| Tuple | (T1, T2, T3) | Fixed-length heterogeneous collection |
| Array | [T; N] | Fixed-length homogeneous collection |
Go developers should note the difference in character types. Rust’s char is a Unicode scalar value (4 bytes), representing any Unicode character, while Go’s rune is also a 4-byte Unicode code point. However, Go’s byte is an alias for uint8 (1 byte), while Rust doesn’t have a separate byte type—use u8 directly.
isize and usize are platform-dependent integer types corresponding to pointer size—32-bit on 32-bit systems, 64-bit on 64-bit systems. This is similar to Go’s int/uint.
String types come in two flavors: &str is a string slice (immutable, borrowed), and String is a heap-allocated string (mutable, owned). This differs from Go’s design of string (immutable) + []byte (mutable). Rust ensures memory safety through borrowing rules, without distinguishing between UTF-8 byte sequences and string types.
Tuple types use parentheses, like (i32, f64, bool). Go has no built-in tuple type but can simulate via structs or multiple return values.
| |
Control Flow
if Expression
Rust’s if is an expression—it can return values:
| |
Go’s if is a statement and cannot return values; Rust’s if is an expression. Note that if expressions must return values of the same type.
| |
Rust’s condition must be of bool type—unlike C/Go, non-zero values are not automatically treated as true.
match Expression
match is Rust’s killer feature, much more powerful than Go’s switch:
| |
Go/Rust Comparison:
| Go | Rust | |
|---|---|---|
| Fallthrough | Fallthrough by default, need break to prevent | No fallthrough, each branch is independent |
| Exhaustiveness | Not required, has default | Required, compiler checks |
| Pattern matching | Limited (type switch) | Powerful (structs, ranges, Option, etc.) |
| Expression | Not an expression | Is an expression, can return values |
Rust’s match must exhaust all possibilities, otherwise compilation fails. This guarantees no case is missed, avoiding the common “forgot default” bug in Go.
Each branch in match uses => to connect pattern and expression. _ is a wildcard matching all unlisted cases. Range matching uses ..= for inclusive ranges, more intuitive than Go’s case.
Option<T> is Rust’s standard library type representing “value or no value”, lighter than Go’s (T, error). Some(T) indicates a value, None indicates no value. Through match, you can elegantly handle both cases.
Loops
Rust has three loop types: loop, while, for:
| |
Go/Rust Comparison:
| Scenario | Go | Rust |
|---|---|---|
| Infinite loop | for { ... } | loop { ... } |
| Conditional loop | for condition { ... } | while condition { ... } |
| Iterate collection | for i, v := range items | for (i, v) in items.iter().enumerate() |
| Index loop | for i := 0; i < n; i++ | for i in 0..n |
Rust’s for loop is based on iterators, more general than Go’s range. Through the iterator pattern, you can iterate over any type implementing the IntoIterator trait.
loop is a Rust feature—infinite loops must have explicit break, avoiding the common “forgot break” infinite loop bug in Go. break can even return values, practical in certain scenarios.
Functions
Rust uses the fn keyword to define functions. Parameters must have type annotations, and return type is specified after ->:
| |
Go/Rust Comparison:
| Feature | Go | Rust |
|---|---|---|
| Function signature | func add(a, b int) int | fn add(a: i32, b: i32) -> i32 |
| Multiple return values | Native support | Via tuple |
| Named return values | Supported | Not supported |
| Omit return value | Can omit (with return type) | Last expression automatically returned |
Rust doesn’t have named return values, but returning via expressions is more concise. Go’s named return values can be assigned directly in the function body, but Rust requires explicit returns.
Statements vs Expressions
This is one of the biggest syntactic differences between Rust and Go. Rust distinguishes between statements and expressions:
- Statement: Performs some action but doesn’t return a value, ends with semicolon
; - Expression: Evaluates and returns a value, doesn’t end with semicolon
| |
In Go, almost everything is a statement, with few exceptions (like func literal). In Rust, if, match, loop, {} blocks can all be expressions, making code more concise.
Key rule: An expression assigned to a variable must not end with a semicolon, otherwise it becomes a statement and returns () (unit type).
| |
References and Slices Basics
Rust’s reference system is the foundation of its ownership model. References allow you to access values without taking ownership:
| |
&s1 creates an immutable reference to s1, and s’s type is &String. When the function body ends, the reference becomes invalid, but s1’s ownership remains unchanged.
Slice syntax [start..end] creates a slice view of a collection. [0..5] means from index 0 (inclusive) to index 5 (exclusive). .. can omit start or end: ..5 means from 0 to 5, 6.. means from 6 to the end.
Go/Rust Comparison:
| Go | Rust | |
|---|---|---|
| Immutable reference | &T or pointer *T | &T |
| Mutable reference | *T | &mut T |
| String slice | s[start:end] | &s[start..end] |
| Borrowing rules | No constraints | Multiple immutable OR one mutable reference in same scope |
Go has no borrowing rules and can have multiple mutable references simultaneously. Rust’s borrowing rules guarantee compile-time memory safety, one of Rust’s core designs.
Ownership Preview
Rust’s ownership is its most unique feature, ensuring memory safety without garbage collection. Basic rules:
- Every value in Rust has an owner (owner)
- A value can have only one owner at any given time
- When the owner goes out of scope, the value is dropped
| |
The String type is heap-allocated, and assignment defaults to move semantics, not shallow copy. Go’s string assignment is shallow copy (sharing underlying array), while Rust requires explicit .clone() for deep copy.
We’ll cover ownership in detail in the next post, including borrowing, lifetime, and other core concepts. For now, just have an impression—Rust guarantees compile-time memory safety through ownership, without garbage collection.
Summary
We’ve now covered the core of Rust basic syntax—variable declaration, basic data types, control flow, functions, statements vs expressions, references and slices, ownership preview. You can now read most Rust code.
If you remember one thing: Rust’s syntax balances expressiveness and safety—variables are immutable by default, match is powerful and must be exhaustive, if/match/loop are expressions, borrowing rules guarantee compile-time memory safety.
In the next post, we’ll dive into Rust’s most unique feature: ownership and borrowing—ownership rules, references, lifetime, Copy trait. This is the foundation of Rust’s safe programming and what Go developers need to adapt to most.