DevToolBoxฟรี
บล็อก

Rust Basics Guide: Ownership, Borrowing, Lifetimes, and Systems Programming

15 min readโดย DevToolBox

Rust Basics Guide: Ownership, Borrowing, Lifetimes, and Systems Programming

A comprehensive guide to Rust programming language fundamentals — ownership, borrowing, lifetimes, traits, error handling, concurrency, and more. Learn why Rust is the future of systems programming.

TL;DR

Rust achieves memory safety through a compile-time ownership system — no garbage collector required — while matching C/C++ performance. The three key concepts are: ownership (each value has exactly one owner), borrowing (temporary access via references), and lifetimes (ensuring references remain valid). Master these three and you can write safe, blazing-fast systems code.

Key Takeaways
  • Ownership rule: each value has one owner; value is dropped when owner goes out of scope
  • Borrow rules: many immutable refs OR exactly one mutable ref — never both simultaneously
  • Lifetime annotations ensure references never outlive the data they point to
  • Result<T, E> and Option<T> replace exceptions and null for explicit error handling
  • Traits provide polymorphism without inheritance; generics provide zero-cost abstractions
  • Arc<Mutex<T>> for shared state across threads; channels for message passing
  • Cargo unifies building, testing, dependency management, and publishing

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety. Since its 1.0 release in 2015, Rust has consistently ranked as the most loved programming language in the Stack Overflow Developer Survey for eight consecutive years. This guide walks you through the core concepts that make Rust unique and powerful.

Why Rust? Memory Safety Without GC, Performance, and Concurrency

Most programming languages choose one of two paths: manual memory management (C, C++) for performance, or garbage collection (Go, Java, Python) for safety. Rust takes a third path — memory safety enforced at compile time through the ownership system, with zero runtime cost.

The Three Pillars of Rust

Rust is built on three core promises:

  • Memory safety — no null pointer dereferences, no use-after-free, no buffer overflows
  • Thread safety — data races are a compile error, not a runtime crash
  • Zero-cost abstractions — high-level code compiles to machine code as efficient as hand-written C

The Ownership System: Rules, Move Semantics, and the Copy Trait

Ownership is Rust's most unique feature and central innovation. Every value in Rust has a single owner, and when that owner goes out of scope, the value is automatically dropped (freed).

The Three Ownership Rules

  1. Each value in Rust has a variable called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
fn main() { // s1 owns the String let s1 = String::from("hello"); // Ownership moves from s1 to s2 let s2 = s1; // ERROR: s1 is no longer valid — it was moved // println!("{}", s1); // error[E0382]: borrow of moved value: `s1` // s2 is valid println!("{}", s2); // "hello" } // s2 is dropped here, memory is freed

Move Semantics vs Copy Semantics

Heap-allocated types like String and Vec are moved by default. Stack-only types that implement the Copy trait are copied instead, so the original remains valid.

fn main() { // i32 implements Copy — both variables are valid let x = 5; let y = x; println!("x = {}, y = {}", x, y); // both work! // String does NOT implement Copy — it moves let s1 = String::from("world"); let s2 = s1; // s1 is moved into s2 // println!("{}", s1); // compile error! // To keep both, use .clone() let s3 = String::from("clone me"); let s4 = s3.clone(); // deep copy println!("s3 = {}, s4 = {}", s3, s4); // both work } // Types that implement Copy: i32, f64, bool, char, tuples of Copy types // Types that do NOT: String, Vec<T>, Box<T>, any type owning heap data

Borrowing and References: &T, &mut T, and the Borrow Checker

Instead of transferring ownership, you can borrow a value by creating a reference. References allow you to refer to a value without taking ownership of it.

Immutable References (&T)

fn calculate_length(s: &String) -> usize { s.len() } fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // borrow s1, don't move it println!("Length of '{}' is {}.", s1, len); // s1 is still valid! // Multiple immutable borrows are fine let r1 = &s1; let r2 = &s1; println!("{} and {}", r1, r2); // both valid simultaneously }

Mutable References (&mut T)

The critical rule: you can only have ONE mutable reference to a piece of data at a time, and you cannot mix mutable and immutable references in the same scope.

fn change(some_string: &mut String) { some_string.push_str(", world"); } fn main() { let mut s = String::from("hello"); change(&mut s); println!("{}", s); // "hello, world" // ERROR: Cannot have two mutable references at once // let r1 = &mut s; // let r2 = &mut s; // error[E0499]: cannot borrow `s` as mutable more than once // NLL (Non-Lexical Lifetimes) — borrow ends at last use let r3 = &s; // immutable borrow starts let r4 = &s; // another immutable borrow println!("{} and {}", r3, r4); // r3 and r4 last used here // r3 and r4 are no longer active let r5 = &mut s; // mutable borrow is now OK r5.push_str("!"); }

Lifetimes: 'a, Lifetime Elision, and 'static

Lifetimes are Rust's way of ensuring that references remain valid for as long as they are used. In most cases the compiler infers lifetimes automatically (lifetime elision), but sometimes explicit annotation is required.

// Explicit lifetime annotation needed: // Rust can't know if the return references x or y without 'a fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // Lifetime in structs struct Important<'a> { part: &'a str, // part must live at least as long as Important } impl<'a> Important<'a> { fn level(&self) -> usize { 3 } } // 'static lifetime — lives for the entire program fn static_example() -> &'static str { "I am always valid" // string literals are 'static } fn main() { let string1 = String::from("long string"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); println!("Longest: {}", result); // OK — both strings live here } // println!("{}", result); // ERROR if uncommented — string2 dropped }

Structs and Enums: impl Blocks, Methods, and Associated Functions

#[derive(Debug, Clone)] struct Rectangle { width: u32, height: u32, } impl Rectangle { // Associated function (constructor) — Rectangle::new(30, 50) fn new(width: u32, height: u32) -> Self { Rectangle { width, height } } // Method — rect.area() fn area(&self) -> u32 { self.width * self.height } fn perimeter(&self) -> u32 { 2 * (self.width + self.height) } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } // Mutable method fn scale(&mut self, factor: u32) { self.width *= factor; self.height *= factor; } } // Enums with data #[derive(Debug)] enum Shape { Circle(f64), Rectangle(f64, f64), Triangle { base: f64, height: f64 }, // named fields } impl Shape { fn area(&self) -> f64 { match self { Shape::Circle(r) => std::f64::consts::PI * r * r, Shape::Rectangle(w, h) => w * h, Shape::Triangle { base, height } => 0.5 * base * height, } } } fn main() { let mut r = Rectangle::new(30, 50); println!("Area: {}, Perimeter: {}", r.area(), r.perimeter()); r.scale(2); println!("Scaled: {:?}", r); let shapes = vec![ Shape::Circle(5.0), Shape::Rectangle(4.0, 6.0), Shape::Triangle { base: 6.0, height: 4.0 }, ]; for s in &shapes { println!("{:?} => area {:.2}", s, s.area()); } }

Pattern Matching: match, if let, while let, and Destructuring

fn describe_number(n: i32) -> &'static str { match n { 0 => "zero", 1 | 2 | 3 => "small positive", 4..=9 => "medium", 10..=99 => "large", n if n < 0 => "negative", _ => "huge", } } fn main() { // Exhaustive matching — compiler forces all cases let val: Option<i32> = Some(42); // match match val { Some(n) if n > 100 => println!("Big: {}", n), Some(n) => println!("Got: {}", n), None => println!("Nothing"), } // if let — concise single-branch match if let Some(n) = val { println!("Shorthand: {}", n); } // while let — pop from stack until empty let mut stack = vec![1, 2, 3]; while let Some(top) = stack.pop() { print!("{} ", top); // 3 2 1 } println!(); // Destructuring tuples, structs let point = (3, 7); let (x, y) = point; println!("x={}, y={}", x, y); struct Pt { x: i32, y: i32 } let p = Pt { x: 10, y: 20 }; let Pt { x, y } = p; println!("x={}, y={}", x, y); // @ bindings — bind while testing let num = 15; match num { n @ 1..=12 => println!("Month: {}", n), n @ 13..=19 => println!("Teen: {}", n), n => println!("Other: {}", n), } }

Error Handling: Result<T, E>, Option<T>, ? Operator, and Custom Errors

Rust has no exceptions. It uses Result<T, E> and Option<T> types to represent fallible operations, making error handling explicit and composable.

use std::fs::File; use std::io::{self, Read}; use std::num::ParseIntError; use std::fmt; // ? operator — propagates errors automatically fn read_file(path: &str) -> Result<String, io::Error> { let mut contents = String::new(); File::open(path)?.read_to_string(&mut contents)?; Ok(contents) } // Custom error type #[derive(Debug)] enum AppError { Parse(ParseIntError), OutOfRange(i32), } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::Parse(e) => write!(f, "parse error: {}", e), AppError::OutOfRange(n) => write!(f, "{} is out of valid range", n), } } } // From trait enables automatic conversion with ? impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::Parse(e) } } fn validate_age(s: &str) -> Result<u8, AppError> { let age: i32 = s.trim().parse()?; // ParseIntError -> AppError via From if !(0..=150).contains(&age) { return Err(AppError::OutOfRange(age)); } Ok(age as u8) } fn main() { // unwrap_or, unwrap_or_else, map, and_then let age = validate_age("25").unwrap_or(0); let doubled = validate_age("21").map(|a| a * 2).unwrap_or(0); // and_then chains Results let result = validate_age("30") .and_then(|a| if a > 18 { Ok(a) } else { Err(AppError::OutOfRange(a as i32)) }); for input in &["25", "-5", "200", "abc"] { match validate_age(input) { Ok(a) => println!("Valid age: {}", a), Err(e) => println!("Error for {:?}: {}", input, e), } } }

Traits: Defining, Implementing, Trait Objects, and Generics

use std::fmt::Display; trait Summary { fn summarize_author(&self) -> String; // required fn summarize(&self) -> String { // default implementation format!("(Read more from {}...)", self.summarize_author()) } } struct Article { title: String, author: String, content: String, } impl Summary for Article { fn summarize_author(&self) -> String { self.author.clone() } fn summarize(&self) -> String { format!("{}, by {} — {}", self.title, self.author, &self.content[..50]) } } // Trait bounds — static dispatch (monomorphization, zero overhead) fn notify<T: Summary + Display>(item: &T) { println!("Breaking news! {}", item.summarize()); } // Where clause for readability fn complex<T, U>(t: &T, u: &U) where T: Summary + Clone, U: Summary + std::fmt::Debug, { println!("{} {}", t.summarize(), u.summarize()); } // Trait objects — dynamic dispatch (runtime polymorphism) fn notify_all(items: &[Box<dyn Summary>]) { for item in items { println!("{}", item.summarize()); } } // Returning trait objects fn make_summarizable(is_article: bool) -> Box<dyn Summary> { if is_article { Box::new(Article { title: String::from("Rust Is Amazing"), author: String::from("Ferris"), content: String::from("Rust achieves memory safety without GC..."), }) } else { // Some other type implementing Summary Box::new(Article { title: String::from("Default"), author: String::from("Anonymous"), content: String::from("Content here..."), }) } }

Iterators and Closures: map, filter, collect, and Chaining

fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Lazy iterator chain — no work until consumed let sum: i32 = numbers .iter() .filter(|&&x| x % 2 == 0) // keep evens: 2,4,6,8,10 .map(|&x| x * x) // square: 4,16,36,64,100 .sum(); // consume: 220 println!("Sum of even squares: {}", sum); // Collect into Vec let strings: Vec<String> = (1..=5) .map(|n| format!("item_{}", n)) .collect(); println!("{:?}", strings); // zip — pair two iterators let names = vec!["Alice", "Bob", "Charlie"]; let scores = vec![95, 87, 91]; let paired: Vec<_> = names.iter().zip(scores.iter()).collect(); println!("{:?}", paired); // flat_map let sentences = vec!["hello world", "foo bar"]; let words: Vec<&str> = sentences.iter() .flat_map(|s| s.split_whitespace()) .collect(); println!("{:?}", words); // fold — general accumulator let product: i32 = (1..=5).fold(1, |acc, x| acc * x); println!("5! = {}", product); // Closures capturing environment let threshold = 5; let above_threshold: Vec<_> = numbers.iter() .filter(|&&x| x > threshold) // captures threshold .collect(); println!("{:?}", above_threshold); // Custom iterator with take_while and skip_while let partitioned: Vec<_> = numbers.iter() .skip_while(|&&x| x < 3) // skip until x >= 3 .take_while(|&&x| x < 8) // take until x >= 8 .collect(); println!("{:?}", partitioned); // [3, 4, 5, 6, 7] }

Cargo and Crates: Cargo.toml, crates.io, and Workspaces

# Cargo.toml — project manifest [package] name = "my_app" version = "0.1.0" edition = "2021" description = "My awesome Rust application" license = "MIT OR Apache-2.0" [dependencies] serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0" clap = { version = "4", features = ["derive"] } log = "0.4" env_logger = "0.10" [dev-dependencies] criterion = "0.5" [profile.release] opt-level = 3 lto = true codegen-units = 1 # Workspace Cargo.toml (monorepo) # [workspace] # members = ["app", "core-lib", "api-types"] # resolver = "2"# Key Cargo commands cargo new my_project # new binary project cargo new my_lib --lib # new library project cargo init # init in existing directory cargo build # debug build (fast compile) cargo build --release # optimized build cargo run -- --arg value # build and run with args cargo test # run all tests cargo test my_test_name # run specific test cargo test -- --nocapture # show stdout from tests cargo bench # run benchmarks cargo doc --open # generate + open docs cargo fmt # format code (rustfmt) cargo clippy # lint with Clippy cargo check # type-check without compiling cargo add serde --features derive # add dependency cargo update # update Cargo.lock

Standard Library: Vec, HashMap, String vs &str, and Box<T>

use std::collections::HashMap; fn main() { // ============ String vs &str ============ let literal: &str = "static string"; // &str — immutable slice, stack let owned: String = String::from("heap string"); // String — heap, mutable let also_owned: String = "convert".to_string(); let slice: &str = &owned; // String to &str via deref coercion // String operations let mut s = String::new(); s.push_str("hello"); s.push(' '); s += "world"; let upper = s.to_uppercase(); let words: Vec<&str> = s.split_whitespace().collect(); let contains_hello = s.contains("hello"); let replaced = s.replace("world", "Rust"); // ============ Vec<T> ============ let mut v: Vec<i32> = vec![3, 1, 4, 1, 5, 9, 2, 6]; v.push(7); v.extend([8, 10]); v.sort(); v.dedup(); v.retain(|&x| x > 3); let sum: i32 = v.iter().sum(); let safe_get: Option<&i32> = v.get(100); // None, not panic // ============ HashMap<K, V> ============ let mut map: HashMap<&str, Vec<i32>> = HashMap::new(); map.entry("alice").or_insert_with(Vec::new).push(95); map.entry("alice").or_insert_with(Vec::new).push(87); map.entry("bob").or_insert_with(Vec::new).push(91); for (name, scores) in &map { let avg: f64 = scores.iter().sum::<i32>() as f64 / scores.len() as f64; println!("{}: avg {:.1}", name, avg); } // ============ Box<T> — heap allocation ============ let boxed: Box<i32> = Box::new(42); println!("Boxed: {}", *boxed); // deref coercion // Box enables recursive types enum Tree { Leaf(i32), Node(Box<Tree>, Box<Tree>), } let tree = Tree::Node( Box::new(Tree::Leaf(1)), Box::new(Tree::Leaf(2)), ); }

Concurrency: Threads, Arc, Mutex, and Channels

Rust's ownership system prevents data races at compile time. The type system ensures that unsafe concurrent access to shared state is impossible without explicit synchronization.

use std::sync::{Arc, Mutex}; use std::thread; use std::sync::mpsc; fn main() { // ============ Basic threads ============ let handle = thread::spawn(|| { println!("Hello from spawned thread!"); }); handle.join().unwrap(); // move closure — take ownership of captured values let data = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Thread has data: {:?}", data); }); handle.join().unwrap(); // ============ Arc<Mutex<T>> — shared mutable state ============ let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let c = Arc::clone(&counter); let h = thread::spawn(move || { let mut num = c.lock().unwrap(); // blocks until lock acquired *num += 1; }); // lock released automatically when num goes out of scope handles.push(h); } for h in handles { h.join().unwrap(); } println!("Counter: {}", *counter.lock().unwrap()); // 10 // ============ Message passing with channels ============ let (tx, rx) = mpsc::channel::<String>(); let tx2 = tx.clone(); thread::spawn(move || { tx.send(String::from("Message from thread 1")).unwrap(); }); thread::spawn(move || { tx2.send(String::from("Message from thread 2")).unwrap(); }); // rx is an iterator over received values for _ in 0..2 { let msg = rx.recv().unwrap(); // blocks until message arrives println!("Received: {}", msg); } }

Rust vs C++ vs Go Comparison

Rust occupies a unique space in the systems programming landscape. Here is how it compares to the two most common alternatives:

FeatureRustC++Go
Memory managementOwnership (compile-time)Manual + RAIIGarbage collected
Memory safetyStrong (compile-time guaranteed)Weak (programmer responsibility)Strong (GC guaranteed)
PerformanceBlazing fast (C-comparable)Blazing fast (C-level)Fast (GC pauses)
ConcurrencyCompile-time data race preventionUnsafe (no guarantees)Goroutines (runtime scheduler)
Learning curveSteep (borrow checker)Very steepGentle
Compile speedSlow (incremental improves)Slow (template-dependent)Very fast
Package managerCargo (excellent)No official (vcpkg/conan)Go modules (built-in)
Null safetyOption<T> (no null)None (std::optional optional)None (nil exists)
Error handlingResult<T,E> (explicit)Exceptions (throw/catch)Multiple returns (error interface)
Best forOS, embedded, WebAssembly, CLIGame engines, drivers, HPCMicroservices, networking, DevOps

Frequently Asked Questions

What makes Rust different from C and C++?

Rust provides memory safety guarantees at compile time through its ownership system, eliminating entire categories of bugs like null pointer dereferences, use-after-free, and data races — without needing a garbage collector. C and C++ rely on programmer discipline for memory safety, while Rust enforces it automatically.

Is Rust hard to learn?

Rust has a steeper learning curve than Go or Python, primarily because of the ownership and borrow checker concepts. However, most developers report that once these concepts click, they become second nature. The Rust Book (doc.rust-lang.org/book) and the Rustlings exercises are excellent free learning resources.

When should I use Rust vs Go?

Use Rust when you need maximum performance, low-level control, or safety-critical systems — OS kernels, embedded systems, game engines, or WebAssembly. Use Go when you need fast development cycles, simple concurrency, and network services — microservices, CLI tools, DevOps utilities.

What is the borrow checker in Rust?

The borrow checker is the Rust compiler component that enforces ownership rules at compile time. It ensures that: (1) references don't outlive the data they refer to, (2) you don't have both mutable and immutable references simultaneously, and (3) there's at most one mutable reference at a time. This eliminates memory bugs without runtime overhead.

What is async/await in Rust?

Rust supports async/await syntax for writing asynchronous code. The async keyword transforms a function into one returning a Future. The await keyword suspends execution until a Future is resolved. Rust does not include an async runtime by default — you typically use Tokio or async-std. Unlike Go, Rust async is zero-cost and does not require separate OS threads.

What is Rust used for in production?

Rust is used in production for: systems programming (OS components, drivers), WebAssembly (Figma, game engines), networking (Cloudflare workers, AWS Firecracker), databases (TiKV, Neon), CLI tools (ripgrep, fd, bat, exa), game development (Bevy engine), and embedded systems. Major adopters include Microsoft, Google, Amazon, Meta, Discord, and Dropbox.

How does Rust handle null values?

Rust has no null keyword. Instead, it uses the Option<T> enum with two variants: Some(T) when a value exists, and None when it does not. This forces you to explicitly handle the absence of a value, eliminating null pointer dereferences entirely. You handle Option using match, if let, unwrap_or, map, and the ? operator.

What is cargo and how do I use it?

Cargo is the official Rust build tool and package manager. Key commands: cargo new project_name (create project), cargo build (compile), cargo run (compile and run), cargo test (run tests), cargo add crate_name (add dependency), cargo doc --open (generate docs), cargo publish (publish to crates.io). Dependencies are declared in Cargo.toml and automatically downloaded from crates.io.

Conclusion: Is Rust Worth Learning?

Rust is not just another systems programming language — it represents a paradigm shift in software engineering. By enforcing memory and thread safety at compile time rather than at runtime, Rust enables developers to write code that is both safe and performant in ways that are difficult or impossible to achieve in other languages.

The learning curve is real, primarily around understanding the borrow checker. But once you internalize the ownership model, you will find that it not only prevents bugs but helps you think more clearly about the structure and data flow of your programs. Rust's compiler errors are famously helpful, often telling you exactly how to fix the issue.

If you are hitting memory safety issues in C++, performance ceilings in Go, or building any system where reliability, efficiency, and concurrency matter — Rust is worth serious consideration. The investment in learning pays dividends in code quality and confidence.

Learning Resources

  • The Rust Book doc.rust-lang.org/book (official free, the best starting point)
  • Rustlings github.com/rust-lang/rustlings (small interactive exercises)
  • Rust by Example doc.rust-lang.org/rust-by-example (learn by reading examples)
  • The Rustonomicon (the dark arts of unsafe Rust)
  • crates.io (the Rust package registry)
  • Rust Playground play.rust-lang.org (run Rust in your browser)
𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

#Hash Generator{ }JSON FormatterB→Base64 Encoder

บทความที่เกี่ยวข้อง

Python Async/Await Guide: asyncio, aiohttp, FastAPI, and Testing

Master Python async programming with asyncio. Complete guide with async/await basics, Tasks, aiohttp client/server, FastAPI integration, asyncpg, concurrent patterns, sync/async bridge, and pytest-asyncio.

คู่มือ TypeScript Generics ฉบับสมบูรณ์ 2026: จากพื้นฐานถึงรูปแบบขั้นสูง

เชี่ยวชาญ TypeScript generics: พารามิเตอร์ชนิด, constraints, conditional types, mapped types, utility types และรูปแบบจริง

Node.js Guide: Complete Tutorial for Backend Development

Master Node.js backend development. Covers event loop, Express.js, REST APIs, authentication with JWT, database integration, testing with Jest, PM2 deployment, and Node.js vs Deno vs Bun comparison.