A Tour of Rust

A quick walkthrough of Rusts syntax and features by writing a function, building a simple web server, and exploring concurrency

This tour is your first real look at Rust in motion. Instead of dry theory, you will build three small programs: a function that uses an external crate, a minimal web server, and a concurrent counter. Each one shows a piece of what makes Rust different — its strict compiler, zero‑cost abstractions, and fearless concurrency — without requiring deep language knowledge first. You can follow along by creating new Cargo projects for each section, or read through and come back later.

By the end, you will have seen actual Rust code solving real problems. The details behind each feature are explored in later chapters, so treat this as a hands‑on map, not a final destination.

A Simple Function

A Rust program starts with fn main, but the language’s personality already shows up in the way we write any function. To see that, we will build a small program that prints an ASCII ferris crab using the ferris-says crate. This is a gentle introduction to dependencies, imports, and a few concepts that will stay with you throughout Rust.

Before writing code, create a fresh Cargo project:

1

Create a new project

Cargo sets up a scaffold with a src/main.rs file and a Cargo.toml manifest.

cargo new hello_ferris
cd hello_ferris
2

Add the ferris-says dependency

You can edit Cargo.toml manually or let Cargo do it. The crate provides a say function that draws a speech bubble around your text.

cargo add ferris-says
3

Replace main.rs with the following code

src/main.rs
use ferris_says::say;
use std::io::{stdout, BufWriter};
fn main() {
    let stdout = stdout();
    let message = String::from("Hello fellow Rustaceans!");
    let width = message.chars().count();
    let mut writer = BufWriter::new(stdout.lock());
    say(&message, width, &mut writer).unwrap();
}
4

Run the program

cargo run

If everything works, you will see a cheerful crab holding your message.

This small program packs a surprising amount of Rust’s surface area. The use keywords bring external items into scope — say from the crate, and stdout plus BufWriter from the standard library. String::from creates an owned, heap‑allocated string, and .chars().count() gives the number of Unicode characters, not bytes.

What does unwrap do?:

The say function returns a Result because writing could fail. Calling .unwrap() means “give me the success value, or crash if there was an error.” In a real application you would handle that error gracefully, but for a quick tour, unwrap keeps the code readable.

Notice that writer is declared with let mut. Bindings in Rust are immutable by default — you must explicitly opt in with mut when you intend to change a value later. Here we need a mutable reference because the BufWriter internally updates its buffer as bytes are written.

Now strip the dependencies away and look at a plain function that checks divisibility:

fn is_divisible(dividend: i32, divisor: i32) -> bool {
    dividend % divisor == 0
}
fn main() {
    let result = is_divisible(10, 5);
    println!("10 divisible by 5? {result}");
}

The last expression in is_divisibledividend % divisor == 0 — lacks a semicolon. In Rust, that makes it the return value. If you added a semicolon, the compiler would complain that the function expects a bool but you gave it () (the unit type, like “nothing”). This expression‑versus‑statement distinction is fundamental: blocks, if expressions, and even loops can evaluate to a value, which influences how you structure logic.

You are seeing Rust’s core in a few lines:

If you compiled and ran both examples, you have already used a dependency, seen the immutable‑by‑default rule, handled a Result, and written a function with an implicit return. That is a solid first lap.

A Simple Web Server

Rust’s standard library includes enough networking to build a single‑threaded HTTP server from scratch — no framework required. The server will listen on a local port, accept connections, and reply with a static “Hello, World!” page. The goal is not production robustness; it is to show how ownership and error handling shape even the smallest I/O programs.

1

Create a new Cargo project

cargo new simple_server
cd simple_server
2

Write the server code in src/main.rs

src/main.rs
use std::io::prelude::*;
use std::net::{TcpListener, TcpStream};
fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    println!("Server listening on port 7878");
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        handle_connection(stream);
    }
}
fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();
    let response = "HTTP/1.1 200 OK\r\n\r\nHello from Rust!";
    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}
3

Start the server and visit it

cargo run

Open http://127.0.0.1:7878 in a browser or use curl. You should see the plain‑text greeting. The server will continue handling new connections until you stop it with Ctrl+C.

A few details matter here even at the tour level. TcpListener::bind returns a Result, which we unwrap for simplicity. The .incoming() method produces an iterator over connection attempts; each item is another Result because a connection might fail (e.g., a client reset). The for loop pattern ensures every accepted stream gets processed, and when the loop ends, all resources are cleaned up automatically.

handle_connection takes ownership of the TcpStream. When the function exits, the stream is dropped — its file descriptor closed — without an explicit close call. Rust’s ownership model guarantees cleanup without a garbage collector.

Unwrap in loops is a quick path to a crash:

A single failed read or write will panic and bring down the entire server. Production code would use match or the ? operator to gracefully handle errors, possibly logging the failure and moving on to the next connection.

The buffer array is stack‑allocated and its size is fixed at compile time. Reading a request into a fixed buffer works for a minimal demo, but a real server would need to handle arbitrarily large headers. Rust pushes you to think about these capacity decisions up front, which is a recurring theme in systems programming.

Even this toy server illustrates a genuine strength: Rust can deliver a functioning network service with zero external crates and predictable memory usage. No garbage collection pauses, no runtime reflection — just an operating system socket and a few safe abstractions.

Concurrency in Action

Rust’s type system does not just keep memory safe in single‑threaded code; it extends the same guarantees to multithreaded programs. The compiler catches data races at build time, which means you can spin up threads without the night‑before‑release dread of Heisenbugs. This section walks through two patterns — spawning threads and sharing mutable state — to make that guarantee tangible.

Start with the simplest possible concurrent program:

use std::thread;
use std::time::Duration;
fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("spawned thread: {i}");
            thread::sleep(Duration::from_millis(10));
        }
    });
    for i in 1..=3 {
        println!("main thread: {i}");
        thread::sleep(Duration::from_millis(15));
    }
    handle.join().unwrap();
}

thread::spawn takes a closure and runs it in a new operating system thread. The call returns a JoinHandle, which we later call .join() on to wait for the spawned thread to finish. If we omitted handle.join(), the main thread might exit before the spawned thread completes, abruptly terminating the whole process.

The output interleaves because the scheduler gives time to both threads. You are seeing true parallelism, not simulated async — and Rust’s borrow checker has already verified that the closure does not hold any references that might become invalid while the main thread continues.

Sharing data without synchronisation will not compile:

If you try to capture a mutable variable from the outer scope and mutate it from two threads, the compiler refuses. For example:

let mut counter = 0;
let handle = thread::spawn(|| {
    counter += 1; // error[E0373]: closure may outlive the current function
});

This is not a runtime check — it is a compile‑time error. Rust forces you to prove that shared data is safe, either by using atomic types, mutexes, or channels.

To share mutable state correctly, you reach for Arc<Mutex<T>> — a reference‑counted pointer wrapped around a mutual exclusion lock. Each thread gets its own clone of the Arc, and the lock ensures only one thread modifies the inner value at a time.

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..5 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Final count: {}", *counter.lock().unwrap());
}

The move keyword transfers ownership of the cloned Arc into the closure, so each thread owns its reference to the shared data. counter.lock() returns a MutexGuard, which dereferences to the inner i32. When the guard goes out of scope, the lock is released automatically. The final count will always be exactly 5 — no lost updates, no torn writes.

Compiled without data races:

If you see "Final count: 5", you have witnessed Rust’s ownership model eliminating an entire class of concurrency bugs at compile time. No runtime instrumentation, no special thread‑safety annotations — just the borrow checker doing its job.

Message passing through channels is an alternative philosophy: “Do not communicate by sharing memory; share memory by communicating.” Rust’s standard library provides multiple‑producer, single‑consumer channels (mpsc) that transfer ownership of the sent value, making it impossible for the original thread to accidentally use it afterward.

use std::sync::mpsc;
use std::thread;
fn main() {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let msg = String::from("hello from the other side");
        tx.send(msg).unwrap();
    });
    let received = rx.recv().unwrap();
    println!("Main got: {received}");
}

Here, msg moves into the channel; after tx.send(msg), the sending thread cannot access msg again. The receiver blocks on recv() until a message arrives. Ownership transfer eliminates any chance of a dangling pointer or double‑free across threads.

These three concurrency examples — raw threads, shared mutable state with Arc<Mutex>, and message passing — are not separate ecosystems. They interoperate, and the same compiler that protects you from use‑after‑free in a single‑threaded function protects you from data races when you introduce parallelism. The concurrency chapter later in the documentation expands on each pattern with deeper reasoning and more realistic workloads.


This tour skimmed the surface of three areas where Rust’s design shows up concretely: function composition with crates, low‑level network I/O without hidden overhead, and multithreaded code that the compiler verifies for safety. The examples are small, but each one touches a principle that runs through the entire language — explicit ownership, no‑cost abstractions, and a compiler that treats mistakes as opportunities to teach rather than things to debug at 2 a.m.