Learning Rust - Writing Your First Rust Program
Episode 3 of 19

Learning Rust - Writing Your First Rust Program

This episode builds your first complete Rust program: the structure of binary and library crates, basic data types, variable immutability and shadowing, functions and expressions, as well as control flow with match and if. All examples can be run directly with cargo.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

After understanding the architecture, it is time to write code that actually compiles. Episode 3 builds the basic syntax of Rust: how programs are structured, how variables and types work, how functions are written as expressions, and how control flow and pattern matching make decisions.

Rust has an expressive yet disciplined syntax. You will see big differences from other languages: variables are immutable by default, functions are expression-based, and match is exhaustive. All examples can be run with cargo run in the project created in episode 0.

Binary and Library Crate Structure

Binary Crate

A binary crate starts from src/main.rs with a main function. Cargo makes it the entry point of the program:

Complete program
cat > src/main.rs <<'EOF'
fn main() {
    let nama = "Rust";
    println!("Halo, {}!", nama);
}
EOF
cargo run

println!("Halo, {}!", nama) prints text with a placeholder. This simple binary crate is enough to explore almost all of the material in episode 3.

Library Crate and Modules

For code that can be tested and reused, use a library crate: src/lib.rs declares public functions that are called from the binary. Modules organize code further:

Libraries and modules
cargo new --lib perpustakaan
mkdir perpustakaan/src/math

The file src/math/kuadrat.rs is called from lib.rs with the declaration pub mod math. Modules keep large codebases structured — a pattern you will use in production projects.

Basic Data Types and Variables

Scalar Types

Rust provides scalar types: fixed-width integers like i32 and u64, floating point f32 and f64, boolean bool, and character char. Types can be inferred automatically by the compiler, but they can still be declared explicitly.

Variables and types
cat > src/main.rs <<'EOF'
fn main() {
    let umur: u32 = 25;
    let harga = 19.99_f64;
    let aktif = true;
    let inisial = 'A';
 
    println!("{} {} {} {}", umur, harga, aktif, inisial);
}
EOF
cargo run

let umur: u32 = 25 declares an unsigned integer. The suffix 19.99_f64 asserts the type of the literal. Most of the time let is enough without a type annotation because the compiler infers it.

Immutability and Shadowing

Rust variables are immutable by default: once bound with let, their value cannot be changed. For values that change, add mut. Shadowing allows you to redeclare a name with a new let, changing the type at the same time:

Shadowing
cat > src/main.rs <<'EOF'
fn main() {
    let nilai = 5;
    let nilai = nilai + 2;
    let nilai = nilai * 3;
 
    println!("hasil: {}", nilai);
 
    let teks = "angka";
    let teks = teks.len();
    println!("panjang: {}", teks);
}
EOF
cargo run

Shadowing creates a new value with the same name without changing the old value. This differs from mut, which changes the value in place. Shadowing is useful for deriving a value step by step without adding new variable names.

Functions and Expressions

Functions with Return Values

Rust functions are declared with fn, parameters are given explicit types, and the return type is written after the arrow:

Functions and expressions
cat > src/main.rs <<'EOF'
fn kuadrat(x: i32) -> i32 {
    x * x
}
 
fn main() {
    let hasil = kuadrat(7);
    let bersyarat = if hasil > 10 { "besar" } else { "kecil" };
 
    println!("{} {}", hasil, bersyarat);
}
EOF
cargo run

Notice x * x without a semicolon: that is the expression that becomes the return value. In Rust, statements end with a semicolon while expressions do not. if is also an expression that produces a value — an idiomatic pattern that replaces the ternary operator.

Expressions vs Statements

The golden rule: no semicolon means expression, semicolon means statement. This understanding matters because it determines the value a function returns. The compiler will even warn you if a let produces nothing.

Control Flow and Pattern Matching

if and loop

Control flow
cat > src/main.rs <<'EOF'
fn main() {
    let mut counter = 0;
 
    loop {
        counter += 1;
        if counter == 3 {
            break;
        }
    }
 
    for i in 0..3 {
        println!("iterasi {}", i);
    }
 
    println!("counter: {}", counter);
}
EOF
cargo run

loop runs endlessly until break, while for iterates over the range 0..3. A range with two dots .. is exclusive: 0..3 covers 0, 1, and 2. for is the most idiomatic way to iterate over collections.

match: Exhaustive Pattern Matching

match compares a value against patterns and runs the first branch that matches. Rust requires exhaustiveness: all possibilities must be handled, or the compiler rejects the program:

Pattern matching
cat > src/main.rs <<'EOF'
fn label(status: u8) -> &'static str {
    match status {
        0 => "pending",
        1 => "proses",
        2 => "selesai",
        _ => "tidak dikenal",
    }
}
 
fn main() {
    println!("{}", label(1));
}
EOF
cargo run

The _ pattern catches all values that do not match. match is Rust's main tool for making value-based decisions — you will use it constantly, especially when handling Result and Option in episode 4.

Closing

Key takeaways:

  • A binary crate starts from src/main.rs; a library crate from src/lib.rs.
  • Variables are immutable by default; use mut to change and a new let for shadowing.
  • Scalar types: integer, float, bool, and char, with automatic type inference.
  • Functions are expression-based: no semicolon to return a value.
  • if, loop, and for are statements; if can also be an expression.
  • match must be exhaustive and is the foundation of pattern matching in Rust.

In the next episode 4 we will discuss error handling and Result — the Result and Option types as error-safe values, the ? operator for error propagation, as well as custom errors with thiserror and anyhow. This is the skill most frequently used in production Rust code.