Rust · Beginner
Rust Syntax and Your First Program
In short: A Rust program is compiled by rustc into a native executable and starts running at fn main(). Statements end with semicolons, braces group blocks, println! is a macro that fills {} placeholders with values, and // starts a comment.
How a Rust program runs
Rust is a compiled language. You write the source in a file ending in .rs, and the compiler rustc translates the whole file into a native executable before anything runs. If the compiler finds a problem, such as a misspelt name or a value of the wrong type, it refuses to produce a program at all and prints an error that names the line. That is the trade you make with Rust: a little more time satisfying the compiler, and in return whole categories of mistakes never reach a running program. Most projects use Cargo, Rust's build tool, but a single file compiles directly with rustc main.rs, which is how every program on this site is checked.
Every executable starts in a function called main. The fn keyword introduces a function, the empty parentheses say it takes no parameters, and the braces enclose its body. Inside, statements end with a semicolon and run from top to bottom. Braces also group the body of an if, a loop or a nested block, and indentation is purely for the reader: the compiler counts braces, not spaces. The convention is four spaces per level, which is what the rustfmt tool produces.
println! writes a line to standard output. The exclamation mark means it is a macro rather than an ordinary function. That distinction matters for two reasons: a macro can accept any number of arguments, and it can inspect the text you give it while compiling, so a placeholder {} without a matching value is a compile-time error instead of a wrong line of output. Each {} is filled with the next argument; a name inside the braces, such as {price}, pulls a variable from the surrounding scope (supported since Rust 1.58), and {:.2} formats a number with two decimal places. print! does the same without the trailing newline.
Two slashes start a comment that runs to the end of the line; /* ... */ comments out a block. The compiler ignores both. Names are case sensitive, and Rust expects variables and functions in snake_case; it will still compile a name like totalWeight, but it warns you.
Reading input takes a few more words than printing. std::io::stdin() gives a handle to standard input, .lock() takes exclusive, buffered access to it, and .lines() turns it into a sequence of lines, each one with the line break already removed. The .lines() method comes from the BufRead trait, so the file needs use std::io::BufRead; near the top to bring that method into scope. Every exercise here reads its data this way and prints its answer with println!.
Syntax
use std::io::BufRead; // brings .lines() into scope
// a line comment: ignored by the compiler
fn main() { // the program starts here
let count = 3; // a statement ends with ;
println!("{} items", count); // a macro call: name!( ... )
let line = std::io::stdin().lock().lines().next().unwrap().unwrap();
println!("You typed {line}"); // a variable named inside the braces
}use lines go at the top of the file. Inside main, each statement ends with a semicolon, and {} in a format string is replaced by the next argument. The .next().unwrap().unwrap() chain takes the first line of input; the error handling lesson explains what the two unwrap() calls do.
Printing with placeholders
Compare the plain {} placeholders with the named {price:.2} and the print! call that does not end its line.
fn main() {
// A notice board for a small bakery
let loaves = 36;
let price = 3.5;
println!("Hollow Oak Bakery");
println!("Loaves on the shelf: {}", loaves);
println!("Price per loaf: {price:.2}");
print!("Today's breads: ");
println!("{}, {} and {}", "rye", "spelt", "seeded");
}Output
Hollow Oak Bakery Loaves on the shelf: 36 Price per loaf: 3.50 Today's breads: rye, spelt and seeded
The first three println! calls each end their line. {} takes the next argument in order, so loaves fills the placeholder in the second line. In the third line the placeholder names the variable directly and adds :.2, so the floating-point value 3.5 is printed with two decimal places as 3.50. print! writes Today's breads: and stops, and the following println! continues on the same line, which is why the last line of output was built by two macro calls. The comment on line 2 produces nothing.
Reading one line of input
The program is run with the single input line Rosscarbery.
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let town = lines.next().unwrap().unwrap();
let town = town.trim();
println!("Next ferry to {}", town);
println!("Name length: {} characters", town.chars().count());
}Input given to the program: Rosscarbery
Output
Next ferry to Rosscarbery Name length: 11 characters
lines() yields one line at a time. next() asks for the first, and the two unwrap() calls say: I expect a line to exist and I expect reading it to succeed; if either is false, stop the program with an error. That is acceptable in a small program where missing input is a genuine bug; the error handling lesson shows the alternatives. trim() removes spaces at either end, and reusing the name town for the trimmed version is ordinary Rust style called shadowing, covered in variables and mutability. Note chars().count() rather than len(): len() returns the number of bytes, which is larger than the number of characters as soon as a name contains a letter such as é.
Blocks, semicolons and an if
Look at the block assigned to total: its last line has no semicolon.
fn main() {
let crates = 4;
let bottles_per_crate = 12;
let total = {
let loose = 5;
crates * bottles_per_crate + loose
};
println!("Bottles to load: {}", total);
if total > 50 {
println!("Use the large van");
}
println!("Loading plan printed");
}Output
Bottles to load: 53 Use the large van Loading plan printed
A pair of braces makes a block, and a block is itself an expression whose value is its final expression, provided that expression has no semicolon. Here the block computes 4 * 12 + 5, and loose exists only inside it. The if that follows needs no parentheses around its condition; the braces around its body are compulsory even for one statement, which removes a classic source of bugs in languages where they are optional. The control flow lesson builds on this.
Common mistakes
Calling println without the exclamation mark
Why it goes wrong:
printlnis a macro, not a function, and the compiler reports error E0423, expected function, found macroprintln. The!is part of the name.Fix: Write
println!(...). The same applies toprint!,format!andvec!.Rust · fixprintln!("ready");Leaving off a semicolon between statements
Why it goes wrong: Rust reads the two lines as one expression and reports that it expected
;. Only the last expression of a block may drop its semicolon, and doing so makes it the value of the block.Fix: End every statement with
;. When the compiler points at the end of a line, that is usually the line that needs one.Printing a value with {} that has no text form
Why it goes wrong:
{}uses theDisplaytrait, which numbers and strings implement but tuples, arrays and most structs do not; error E0277 says the type doesn't implementstd::fmt::Display.Fix: Use
{:?}, theDebugformat, for those values, or print each part separately.Rust · fixlet pair = (3, 4); println!("{:?}", pair);Calling .lines() without use std::io::BufRead
Why it goes wrong:
lines()is defined by theBufReadtrait, and a trait's methods are only callable when the trait is in scope. The error is E0599, no method namedlinesfound, and the compiler's help text names the missing import.Fix: Add
use std::io::BufRead;at the top of the file.Rust · fixuse std::io::BufRead; fn main() { for line in std::io::stdin().lock().lines() { println!("{}", line.unwrap()); } }
The pieces of a first program
| Piece | What it does |
|---|---|
fn main() { } | the entry point; execution starts here |
; | ends a statement |
let name = value; | binds a name to a value |
println!("{}", x) | macro that writes one line; {} takes the next argument |
{:?} | placeholder using the Debug format, for tuples, arrays and structs |
// | comment to the end of the line |
use std::io::BufRead; | brings the trait that provides .lines() into scope |
Where you use this
Rust's first natural home is the command-line tool: a program that reads a stream of text, transforms it and prints the result, compiled once into a single fast executable with no runtime to install. A tool that reads lines of a log and prints those above a threshold, or converts a list of measurements to another unit, needs nothing more than fn main, a loop over stdin().lock().lines() and println!. Because the compiler checks every format string and every name before the program exists, the executable you copy to a server does not contain a typo waiting to be reached at three in the morning. Every exercise on this site is that same shape in miniature: read standard input, compute, print.
use std::io::BufRead;
fn main() {
for line in std::io::stdin().lock().lines() {
println!("> {}", line.unwrap());
}
}Key points
rustccompiles the whole file to a native executable before anything runs; an error stops the build.- Execution starts in
fn main(); braces group blocks and semicolons end statements. println!andprint!are macros: the!is part of the name, and{}placeholders are checked at compile time.//starts a comment; names are case sensitive and written in snake_case.- Read input with
std::io::stdin().lock().lines(), which needsuse std::io::BufRead;. - A block's last expression, written without a semicolon, is the value of the block.
Try it yourself
The program reads a town name and prints Ferry to <town>. Add a second line that prints Ticket code: followed by the name in upper case, using the to_uppercase() method on the trimmed name.
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let town = lines.next().unwrap().unwrap();
println!("Ferry to {}", town.trim());
}
SherkinFerry to Sherkin
Ticket code: SHERKINuse std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let town = lines.next().unwrap().unwrap();
println!("Ferry to {}", town.trim());
println!("Ticket code: {}", town.trim().to_uppercase());
}
Practise this
Related lessons
- Variables and Mutability in RustWhy Rust variables are immutable by default, when to write let mut, how shadowing with let creates a new variable, and how const differs from let.7 min
- Data Types in RustRust's scalar and compound types: integer and float sizes, bool, char, tuples and arrays, plus casting with as and parsing numbers from input text.9 min
- Functions in RustHow to define and call Rust functions: typed parameters, the -> return type, returning the last expression, early return, tuples, and &str arguments.8 min
Frequently asked questions
Why do println and print in Rust end with an exclamation mark?
Because they are macros, not functions. A macro is expanded by the compiler before type checking, which lets println! accept any number of arguments and verify the format string against them at compile time. An ordinary Rust function has a fixed parameter list and could do neither. The ! tells you, and the compiler, that macro rules apply.
Do I need Cargo to run a single Rust file?
No. rustc main.rs compiles one file into an executable named main (or main.exe on Windows) that you run directly. Cargo becomes worthwhile as soon as a project has more than one file or uses external crates: it manages dependencies, builds and tests with one command. The programs in these lessons use only the standard library, so either route works.
Why does reading a line in Rust need lock(), lines(), next() and two unwrap() calls?
Each call does one honest job. stdin() is the shared handle, lock() claims it and adds buffering, lines() splits the stream into lines, and next() fetches one. next() returns an Option because input can end, and inside it is a Result because reading can fail, so two unwrap() calls say you accept a crash in both cases. Longer programs replace unwrap() with proper handling, covered in the error handling lesson.
How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.