Introduction to Rust
last modified October 25, 2026
This is an introduction to the Rust programming language. Rust started as a personal project of Graydon Hoare, was sponsored by Mozilla, and was announced in 2010. The first stable release, Rust 1.0, appeared in May 2015. Today the language is developed in the open by the Rust Project, an international community of volunteers and companies; the non-profit Rust Foundation (founded in 2021) supports the project. The website of the language is https://www.rust-lang.org/.
An online development environment is available at https://play.rust-lang.org/.
Rust Programming Language
Rust is a compiled, statically typed systems programming language focused on safety, speed, and concurrency. It allows us to write reliable and efficient software without the overhead of a garbage collector. Its rich type system and ownership model guarantee memory safety and thread safety, eliminating many classes of bugs, such as null pointer dereferences, use-after-free errors, and data races, at compile time.
Rust is especially well suited for performance-critical applications, such as operating systems, game engines, web browsers, and embedded devices. Thanks to its powerful abstractions and zero-cost principles, Rust also excels in WebAssembly, networking, and command-line tools. Rust code is used in projects such as Firefox, the Linux kernel, and Android.
Key features of Rust include:
- Ownership, borrowing, and lifetimes for memory safety without garbage collection.
- Powerful pattern matching and algebraic data types (enums).
- Strong static typing with type inference.
- Zero-cost abstractions – high-level code compiles to fast machine code.
- Fearless concurrency through ownership rules that prevent data races.
- Traits and generics for code reuse and polymorphism.
- Cargo – the built-in package manager and build system.
- Comprehensive tooling: rustfmt, clippy, and integrated testing.
A new stable version of the compiler is released every six weeks. Backward incompatible changes are collected in editions, which are published about every three years (2015, 2018, 2021, and 2024). Crates of different editions can be combined in one program. The examples in this tutorial were tested with Rust 1.97 and the 2024 edition.
Rust installation
The recommended way to install Rust is through rustup, the Rust
toolchain installer. For current instructions, see the official installation
page https://www.rust-lang.org/tools/install.
On Linux, macOS, and other Unix-like systems, we can install Rust with the
following command. On Windows, we download and run the rustup-init.exe
installer.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
The installer puts the compiler rustc, the build tool
cargo, the standard library, and the tools rustfmt
and clippy into the ~/.cargo/bin directory. The
rustup update command installs newer versions later. After the
installation, we verify the installed versions with the --version
option.
$ rustc --version rustc 1.97.1 (8bab26f4f 2026-07-14) $ cargo --version cargo 1.97.1 (c980f4866 2026-06-30)
For VS Code, install the rust-analyzer extension, which provides language support, code completion, inline hints, and debugging capabilities.
Rust simple example
The following is a simple example in Rust. We save it in a file named
main.rs.
fn main() {
println!("First program in Rust");
}
The program prints a message to the console. Rust source files use the
.rs extension. The main function is the entry point
of every Rust program. The function is defined with the fn
keyword; it takes no parameters and returns no value.
The body of the function is enclosed in curly brackets. The
println! macro prints a string to the standard output, followed
by a newline. The exclamation mark indicates that it is a macro, not a regular
function. Statements are terminated with a semicolon. By convention, Rust
code is indented with four spaces.
We compile the program with the rustc compiler and run the
resulting executable. On Windows, the executable is main.exe.
$ rustc main.rs $ ./main First program in Rust
Creating a project with Cargo
Real programs are built with Cargo. The cargo new command
creates a new project in a directory of the given name. The directory contains
a Cargo.toml file, which describes the package and lists its
dependencies, and a src directory with a main.rs
file that already contains a "Hello, world!" program. Cargo also initializes
a Git repository (unless the directory is already inside one), which is why
there is a .git directory and a .gitignore file. The compiled files are later placed in the
target directory, which is ignored by Git.
In the example, we create a project and list its files. The file
Cargo.toml contains the name of the package, its version, and the
edition of the language.
$ cargo new hello
Creating binary (application) `hello` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ ls -A hello
Cargo.toml
.git
.gitignore
src
$ ls hello/src
main.rs
$ cat hello/Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
$ cat hello/src/main.rs
fn main() {
println!("Hello, world!");
}
Building and running with Cargo
The cargo run command compiles the project and runs the program.
The compiler messages go to the standard error output, and the output of the
program goes to the standard output.
We run the project twice. The second time Cargo sees that nothing has changed,
so it skips the compilation. The -q option, which we use in all
other examples, suppresses the messages of Cargo and shows only the output of
the program.
$ cd hello
$ cargo run
Compiling hello v0.1.0 (/home/user/projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/hello`
Hello, world!
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello`
Hello, world!
Checking and release builds
The cargo check command only checks that the code compiles, which
is faster than a full build. The cargo build command compiles
without running; it creates an unoptimized debug build in
target/debug. The --release option turns on
optimizations and places the result into target/release.
In the example, we check and build a release version, and list the contents of
the target directory. The release build takes longer to compile
but produces faster programs; the debug build is meant for development. The
Cargo tutorial covers dependencies, profiles, and
workspaces.
$ cd hello
$ cargo check
Checking hello v0.1.0 (/home/user/projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
$ cargo build --release
Compiling hello v0.1.0 (/home/user/projects/hello)
Finished `release` profile [optimized] target(s) in 0.06s
$ ls target
CACHEDIR.TAG
debug
release
Rust variables
Variables store values, and by default they are immutable.
fn main() {
let name = "John Doe";
let age = 34;
println!("{name} is {age} years old");
let mut visits = 0;
visits += 1;
visits += 1;
println!("{name} has visited us {visits} times");
let age = age + 1;
println!("{name} will be {age} next year");
}
In the example, we declare a string and an integer variable.
let name = "John Doe"; let age = 34;
The let keyword creates a variable, and Rust infers the types
automatically: &str for the string literal and
i32 for the integer. Variable bindings are immutable by default.
let mut visits = 0; visits += 1;
To make a variable mutable, we use let mut. The compiler refuses
to compile a program that changes an immutable variable.
let age = age + 1;
Declaring a variable with an existing name is called shadowing. The new variable hides the old one. See the variable and mutability tutorials for details.
println!("{name} is {age} years old");
Rust uses placeholders like {} inside the format string to
insert values. A variable name inside the braces is inserted directly; this
works for plain variable names. Expressions are passed as separate arguments,
as in println!("{}", a + b).
$ cargo run -q John Doe is 34 years old John Doe has visited us 2 times John Doe will be 35 next year
Rust data types
Rust is statically typed; the type of every value is known at compile time. The compiler can usually infer it, but we can also write it after a colon. The built-in scalar types are:
| Type | Description |
|---|---|
i8, i16, i32, i64, i128, isize | Signed integers |
u8, u16, u32, u64, u128, usize | Unsigned integers |
f32, f64 | Floating-point numbers |
bool | true or false |
char | A Unicode scalar value, written in single quotes |
The number in the name of an integer type is its size in bits. The
isize and usize types have the size of a pointer
(64 bits on a 64-bit system); usize is used for indexes and
lengths. The default types are i32 and f64.
fn main() {
let a: i32 = -42;
let b: u8 = 255;
let c: f64 = 2.5;
let d: bool = true;
let e: char = 'Z';
let s: &str = "Rust";
let t: (i32, &str) = (1, "one");
let arr: [i32; 3] = [1, 2, 3];
println!("i32: {a}, {} bytes", size_of_val(&a));
println!("u8: {b}, {} byte", size_of_val(&b));
println!("f64: {c}, {} bytes", size_of_val(&c));
println!("bool: {d}, {} byte", size_of_val(&d));
println!("char: {e}, {} bytes", size_of_val(&e));
println!("&str: {s}, {} bytes", size_of::<&str>());
println!("tuple: {t:?}, {} bytes", size_of_val(&t));
println!("array: {arr:?}, {} bytes", size_of_val(&arr));
}
In the example, we declare values of the basic types and print them with their sizes.
let t: (i32, &str) = (1, "one"); let arr: [i32; 3] = [1, 2, 3];
A tuple groups values of different types, and an array holds a fixed number of
values of the same type; its length is part of the type. Collections that can
grow, such as Vec and String, are covered in the
vectors and strings
tutorials.
println!("i32: {a}, {} bytes", size_of_val(&a));
The size_of_val function returns the size of a value in bytes. A
char takes four bytes, because it can hold any Unicode scalar
value. A &str is a pointer and a length, so it takes 16 bytes on
a 64-bit system, whatever the length of the text. The tuple takes 24 bytes: 4
for the integer, 16 for the string slice, and 4 bytes of padding. The sizes of
usize, isize, and references depend on the platform.
$ cargo run -q i32: -42, 4 bytes u8: 255, 1 byte f64: 2.5, 8 bytes bool: true, 1 byte char: Z, 4 bytes &str: Rust, 16 bytes tuple: (1, "one"), 24 bytes array: [1, 2, 3], 12 bytes
Rust functions and control flow
Functions are defined with the fn keyword. The types of the
parameters and the return value are always written. The last expression of a
function, without a semicolon, is its return value.
fn square(n: i32) -> i32 {
n * n
}
fn describe(n: i32) -> &'static str {
match n % 3 {
0 => "divisible by three",
1 => "remainder one",
_ => "remainder two",
}
}
fn main() {
for i in 1..=5 {
if i % 2 == 0 {
println!("{i} is even, square {}, {}", square(i), describe(i));
} else {
println!("{i} is odd, square {}, {}", square(i), describe(i));
}
}
}
In the example, we define two functions and call them in a loop.
fn square(n: i32) -> i32 {
n * n
}
The function takes an i32 and returns an i32. The
expression n * n has no semicolon, so its value is returned.
match n % 3 {
0 => "divisible by three",
1 => "remainder one",
_ => "remainder two",
}
The match expression compares a value with patterns. The
underscore matches everything else, and the compiler checks that all cases
are covered. The result of the expression is the return value of the function.
for i in 1..=5 {
if i % 2 == 0 {
...
} else {
...
}
}
The range 1..=5 includes both ends; 1..5 would stop
at 4. The conditions of if need no parentheses but must be of
type bool. See the functions,
control flow, and
match expression tutorials for more.
$ cargo run -q 1 is odd, square 1, remainder one 2 is even, square 4, remainder two 3 is odd, square 9, divisible by three 4 is even, square 16, remainder one 5 is odd, square 25, remainder two
Rust compiler as a teacher
The ownership rules are what makes Rust unique. Every value has exactly one owner, and when the owner goes out of scope, the value is dropped. Assigning a value that owns heap memory to another variable moves it.
fn main() {
let first = String::from("Rust");
let second = first;
println!("{first} {second}");
}
The program tries to use a String after it was moved.
let second = first;
println!("{first} {second}");
The String was moved to second, and
first is no longer valid. In C, such code would compile and
may lead to a double free or to the use of freed memory; in Rust, the program
is rejected with error E0382. Rust error messages show the relevant lines,
explain what happened, and often suggest a fix. Here we can clone the string
or borrow it. The ownership and borrowing
tutorial explains these rules in detail.
$ cargo run -q
error[E0382]: borrow of moved value: `first`
--> src/main.rs:5:16
|
2 | let first = String::from("Rust");
| ----- move occurs because `first` has type `String`, which does not implement the `Copy` trait
3 | let second = first;
| ----- value moved here
4 |
5 | println!("{first} {second}");
| ^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let second = first.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
Rust user input
The std::io module provides input/output operations for the
console.
use std::io::{self, Write};
fn main() {
print!("Enter your name: ");
io::stdout().flush().expect("cannot flush stdout");
let mut name = String::new();
io::stdin()
.read_line(&mut name)
.expect("failed to read a line");
let name = name.trim();
println!("Hello {name}");
}
The example prompts the user for a name and prints a greeting.
use std::io::{self, Write};
We import the std::io module and the Write trait.
The self keyword is used to import the module itself.
print!("Enter your name: ");
io::stdout().flush().expect("cannot flush stdout");
We use the print! macro to write a prompt without a newline.
Because the standard output is line-buffered, we flush it manually with
flush. Otherwise the prompt might not be visible before the
program waits for input. The flush method returns a
Result; the expect method ends the program with the
given message if an error occurs.
let mut name = String::new();
io::stdin()
.read_line(&mut name)
.expect("failed to read a line");
We create a mutable, empty String to hold the input. The
read_line method appends the line typed by the user, including
the trailing newline, to the string. It takes a mutable reference, written
&mut name, so the method can modify our string. It returns a
Result with the number of bytes read.
let name = name.trim();
We remove the trailing newline by calling trim. This returns a
string slice (&str) without the surrounding whitespace. The
new name shadows the old one. The output below was recorded in a
terminal; the user typed "Peter".
$ cargo run -q Enter your name: Peter Hello Peter
Rust parsing user input
Input is always text. To get a number, we parse the string. Parsing can fail,
so parse returns a Result, which we handle with
match.
use std::io::{self, Write};
fn main() {
print!("Enter your age: ");
io::stdout().flush().expect("cannot flush stdout");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("failed to read a line");
match input.trim().parse::<u32>() {
Ok(age) => println!("Next year you will be {}", age + 1),
Err(e) => println!("Invalid age: {e}"),
}
}
The program reads an age and prints the age of the next year.
match input.trim().parse::<u32>() {
Ok(age) => println!("Next year you will be {}", age + 1),
Err(e) => println!("Invalid age: {e}"),
}
The type in the turbofish ::<u32> tells parse what
to produce. The result is either Ok with the number or
Err with a description of the problem. The program does not
crash on wrong input; it reports the error. We run it twice: first with a
valid number and then with text.
$ cargo run -q Enter your age: 34 Next year you will be 35 $ cargo run -q Enter your age: abc Invalid age: invalid digit found in string
Where to go next
The following tutorials continue from here:
- Rust variable, Rust mutability, and operators and expressions
- Control flow, match expression, and functions
- Strings, arrays, vectors, and HashMap
- Ownership and borrowing, lifetimes, and slices
- Structs, enums, traits, and generics
- Error handling, Option type, and Cargo
Source
The Rust Programming Language (The Book)
This was an introduction to the Rust programming language.
Author
List all Rust tutorials.