Lesson 1 of 22·12 min

Getting Started with Rust

Every Rust program you write in this track starts the same way: a project created by Cargo, a main function, and a line of output. This lesson sets up that loop. You will install the toolchain, create and run a project, and learn how println! formats values, which you will use in every lesson after this one.

Installing Rust

Rust is installed with rustup, a small tool that downloads the compiler and keeps it up to date. On macOS and Linux, run:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

On Windows, download and run rustup-init.exe from rustup.rs. Once it finishes, open a new terminal and check that the tools are on your PATH:

rustc --version
cargo --version

rustup installs three things you will use constantly:

  • rustc - the compiler. You rarely call it directly.
  • cargo - the build tool and package manager. This is what you run day to day.
  • rustup itself - run rustup update now and then to get the latest stable release.

Creating a project with Cargo

Cargo creates a new project with one command:

cargo new hello
cd hello

That gives you this layout:

hello
├── Cargo.toml
└── src
    └── main.rs

Cargo.toml is the project's manifest. It holds the package name, its version, the Rust edition it is written against, and later the libraries (crates) it depends on:

[package]
name = "hello"
version = "0.1.0"
edition = "2024"
 
[dependencies]

src/main.rs is the program itself. Cargo fills it with a working example:

fn main() {
    println!("Hello, world!");
}

cargo new also runs git init for you, with a .gitignore that excludes the target/ directory where build output goes.

Running your program

cargo run compiles the project and runs the result:

cargo run
   Compiling hello v0.1.0 (/home/you/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/hello`
Hello, world!

The first three lines come from Cargo; the last is your program. There are two other commands worth knowing from day one:

  • cargo build compiles without running. The binary lands in target/debug/hello.
  • cargo check type-checks the code without producing a binary. It is much faster than a full build, so it is what you run while fixing compiler errors.

When you ship a program, cargo build --release turns on optimisations and puts the binary in target/release/ instead.

Anatomy of main

Look again at the generated file:

fn main() {
    println!("Hello, world!");
}
  • fn declares a function. main is special: it is where every Rust binary starts.
  • The body goes between curly braces.
  • println!("..."); is a statement, and statements end with a semicolon.

The ! after println matters. It means println! is a macro, not a function. A macro is code that writes code: before your program is compiled, the macro call is expanded into ordinary Rust. println! is a macro because it needs things a normal function cannot do, such as accepting any number of arguments and checking your format string against them at compile time. You will write your own macros near the end of this track; for now, just remember that a name ending in ! is a macro call.

Format strings

The first argument to println! is a format string. Each {} in it is a placeholder that is filled with the next argument:

fn main() {
    let name = "Ferris";
    let legs = 10;
 
    println!("Hello, {}!", name);
    println!("{} has {} legs", name, legs);
    println!("{name} has {legs} legs");
    println!("{0} likes {1}, and {1} likes {0}", "Ferris", "Rust");
}
Hello, Ferris!
Ferris has 10 legs
Ferris has 10 legs
Ferris likes Rust, and Rust likes Ferris

Line by line:

  • {} placeholders are filled in order.
  • {name} puts a variable in the string directly. This is usually the most readable form, and it works for plain variable names (not expressions like legs + 1).
  • {0} and {1} refer to arguments by position, so you can reuse one.

Don't worry about let yet; the next lesson covers variables.

print!, eprintln! and escaping braces

println! has a few siblings:

fn main() {
    print!("Loading");
    print!(".");
    print!(".");
    println!(".");
    println!("Done");
    eprintln!("this line goes to stderr");
    println!("{{braces}} are escaped by doubling them");
}
Loading...
Done
this line goes to stderr
{braces} are escaped by doubling them
  • print! writes without a newline at the end.
  • eprintln! (and eprint!) write to standard error instead of standard output. Use them for error messages and diagnostics, so they don't get mixed into output that another program might read.
  • To print a literal brace, double it: {{ and }}.

Debug formatting with {:?}

A plain {} uses the Display trait, which is meant for output aimed at users. Many types, like arrays and tuples, don't implement it, because there is no single obvious way to show them to a user. They do implement Debug, which is output aimed at you, the programmer. Ask for it with {:?}:

fn main() {
    let numbers = [1, 2, 3];
    let pair = ("Ferris", 10);
 
    println!("{:?}", numbers);
    println!("{pair:?}");
    println!("{pair:#?}");
}
[1, 2, 3]
("Ferris", 10)
(
    "Ferris",
    10,
)

{:#?} is the "pretty" form that spreads the value over several lines, which helps once values get large. When you define your own types later, you will make them printable with #[derive(Debug)].

Width, alignment and precision

Anything after the : in a placeholder controls how the value is laid out:

fn main() {
    let pi = 3.14159265;
    println!("{:.2}", pi);
    println!("[{:>6}]", "rust");
    println!("[{:<6}]", "rust");
    println!("[{:^6}]", "rust");
    println!("[{:05}]", 42);
}
3.14
[  rust]
[rust  ]
[ rust ]
[00042]

.2 sets two decimal places, >, < and ^ align right, left and centre within the given width, and a leading 0 pads numbers with zeros. You won't need these often, but they are handy for tables and reports. The full syntax is in the std::fmt docs.

Common mistakes

Forgetting the !

Without the !, Rust looks for a function called println and doesn't find one:

fn main() {
    println("Hello, Ferris!");
}

The compiler tells you exactly what to add. Rust's error messages are usually this helpful, so read them all the way to the help: line before changing anything.

Placeholders and arguments don't match

Because println! is a macro, it counts your placeholders at compile time. One too few arguments is an error, not a runtime surprise:

fn main() {
    let name = "Ferris";
    println!("{} is {} years old", name);
}

Passing an extra argument that no placeholder uses is an error too.

Using {} for a type without Display

fn main() {
    let numbers = [1, 2, 3];
    println!("{}", numbers);
}

The note: line has the fix: switch to {:?}.

A missing semicolon

Statements inside a function end with ;. Leave one off between two statements and the compiler points at the exact spot:

error: expected `;`, found `println`
 --> src/main.rs:2:22
  |
2 |     println!("first")
  |                      ^ help: add `;` here
3 |     println!("second");
  |     ------- unexpected token

Summary

  • Install Rust with rustup; use cargo new, cargo run, cargo build and cargo check to work with projects.
  • Every binary starts at fn main().
  • println! is a macro (note the !). It prints a line; print! skips the newline; eprintln! writes to stderr.
  • {} uses Display, {:?} uses Debug, and {:#?} pretty-prints.
  • Placeholders can be positional ({}), indexed ({0}) or named ({name}), and the compiler checks them against the arguments.
  • Read compiler errors to the end: the help: and note: lines usually tell you the fix.

Practise what you read

Challenges that use the ideas from this lesson.