Getting Started: The Rust Playground
Before writing local code, use the Rust Playground at play.rust-lang.org. Paste any example from this lesson and run it in your browser. No install needed.
Variables and Mutability
In most languages, variables can be changed after you set them. In Rust, they cannot unless you say so explicitly.
let x = 5; // x is 5 and stays 5 forever let mut y = 5; // y starts at 5 y = 15; // y is now 15 — allowed because of mut
If you try to change an immutable variable, the compiler stops you before your code even runs.
Constants are a stronger form of immutable. They require an explicit type and cannot be set to a value computed at runtime:
const MAX_POINTS: u32 = 100_000;
Data Types
Rust needs to know the type of every value at compile time. Usually it figures this out on its own, but you can always be explicit.
Integers
u8,u16,u32,u64— unsigned (positive numbers only, from 0 up)i8,i16,i32,i64— signed (positive and negative)- The number is the bit size.
u64holds up to ~18 quintillion.u8holds 0 to 255.
let amount: u64 = 1_000_000; // SOL amount in lamports let delta: i64 = -500; // a change that could go negative
Floats
f32,f64— numbers with decimal points. Default isf64.
Booleans
let is_active: bool = true;
Characters
let letter: char = 'A'; // single quotes for char, double for stringsArrays — fixed size, all same type
let arr: [i32; 3] = [1, 2, 3]; // exactly 3 signed 32-bit integersTuples — fixed size, can mix types
let t: (i32, f64, bool) = (42, 3.14, true);
let first = t.0; // access by indexStrings
Two kinds of strings in Rust. They behave differently and are used for different things.
String: owned, growable, stored on the heap
let mut s = String::from("hello");
s.push_str(", world"); // s is now "hello, world"&str: a borrowed string slice — a read-only window into existing string data
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello" — a view, not a copyWhen to use which:
- Building or modifying a string → use
String - Just reading a string → use
&str - Function parameters that only need to read → use
&str(more flexible)
Control Flow
If / else
let x = 10;
if x > 5 {
println!("big");
} else {
println!("small");
}
// You can assign from an if expression
let result = if x > 5 { "big" } else { "small" };Loops
// loop — runs forever until break loop { if done { break; } } // while let mut n = 3; while n > 0 { println!("{}", n); n -= 1; } // for — iterating a range or collection for i in 0..5 { println!("{}", i); // prints 0, 1, 2, 3, 4 }
Match — like a switch but must cover every case
let number = 3;
match number {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("something else"), // _ is the wildcard
}Match is used constantly in Rust for working with Result, Option, enums, and error codes.
Functions
fn add(a: i32, b: i32) -> i32 {
return a + b; // explicit return
}
fn square(x: i32) -> i32 {
x * x // implicit return — no semicolon = return this value
}
let result1 = add(5, 3); // 8
let result2 = square(5); // 25Rules:
- Parameter types are always required
- Return type goes after
->— omit if nothing is returned - Last expression without a semicolon is returned automatically
Structs
Structs group related fields into one named type.
struct User {
username: String,
email: String,
active: bool,
}
// Create an instance
let user1 = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
active: true,
};
println!("{}", user1.username); // "alice"Structs can contain other structs and enums. In Anchor, your on-chain account data is always a struct.
Enums
Enums define a type that can be exactly one of several named variants. Each variant can carry different data.
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
Unknown,
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
let nowhere = IpAddr::Unknown;The power of enums is that match forces you to handle every variant:
match home {
IpAddr::V4(a, b, c, d) => println!("{}.{}.{}.{}", a, b, c, d),
IpAddr::V6(s) => println!("{}", s),
IpAddr::Unknown => println!("no address"),
}Ownership
This is what makes Rust different. Three rules:
- Every value has one owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
let s3 = String::from("block");
{
let s4 = s3; // s3 is moved to s4 — s3 no longer valid
println!("{}", s4); // fine
} // s4 goes out of scope — string is dropped
println!("{}", s3); // COMPILER ERROR — s3 was movedThe compiler tracks ownership. When a value is dropped, Rust frees the memory with no garbage collector needed.
Copy types (integers, booleans, floats) are copied automatically, not moved. The move only happens with heap-allocated types like String and Vec.
References and Borrowing
References let you use a value without taking ownership. You borrow it, use it, give it back.
Immutable reference — read-only
fn calculate_length(s: &String) -> usize {
s.len() // can read s, but not change it
}
let s1 = String::from("hello");
let len = calculate_length(&s1); // pass a reference with &
// s1 is still valid hereMutable reference — read and write
fn change(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
change(&mut s);The borrowing rules:
- You can have many immutable references at the same time
- You can have only one mutable reference at a time
- You cannot have a mutable reference and an immutable reference at the same time
These rules are enforced at compile time. They prevent data races before your code runs.
Traits
Traits define behavior that types can share. Like an interface in other languages.
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
headline: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}", &self.content[..50])
}
}&self means the function gets a reference to the struct. It can read the fields but the struct keeps ownership.
Traits can have default implementations. If you do not override a function, the default runs.
Lifetimes
Lifetimes tell the compiler how long a reference must stay valid. They prevent you from using a reference after the data it points to has been dropped.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}'a is a lifetime annotation. It says the returned reference lives as long as the shorter of x and y.
You will see lifetimes constantly in Anchor: Account<'info, T>, Context<'_, '_, '_, 'info, T>. You rarely write them yourself, but you must be able to read them.
Error Handling
Result<T, E>: a value that is either success or failure
fn divide(a: f64, b: f64) -> Result{ if b == 0.0 { Err(String::from("division by zero")) } else { Ok(a / b) } } match divide(10.0, 2.0) { Ok(result) => println!("Result: {}", result), Err(e) => println!("Error: {}", e), }
Option<T>: a value that may or may not exist
fn find_char(c: char, s: &str) -> Option{ for (i, ch) in s.chars().enumerate() { if ch == c { return Some(i); } } None }
Use Result when something can fail with an error. Use Option when something can be absent without that being an error.
Project Structure
my-project/ Cargo.toml ← package manifest: name, version, dependencies src/ main.rs ← binary crate entry point lib.rs ← library crate entry point (used by Anchor) my_module.rs ← a module
cargo new my-project # create a new binary crate cargo new my-library --lib # create a library crate cargo run # compile and run
Modules are declared with mod and can be public or private:
pub mod instructions; // public module in its own file mod helpers; // private module
Every Anchor program is a library crate. lib.rs is the entry point.