Frontend Labs
⋯

Part II — Intermediate: traits, lifetimes, and the standard library

Part I gave you values and ownership. Part II covers abstraction: traits (Rust's interfaces), generics, closures, and iterators. It also covers the part of the borrow checker that Part I avoided, lifetimes. By the end you can read most library code on crates.io.


16. Traits and generics: shared behavior without inheritance

16.1 Defining and implementing a trait

trait Shape {
    fn area(&self) -> f64;                       // required method
    fn name(&self) -> String { "shape".into() }  // default method, can be overridden
    fn describe(&self) -> String {               // defaults can call required methods
        format!("{} with area {:.2}", self.name(), self.area())
    }
}

struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
    fn name(&self) -> String { "circle".into() }
}
impl Shape for Rect {
    fn area(&self) -> f64 { self.w * self.h }
}

fn main() {
    println!("{}", Circle { r: 1.0 }.describe()); // circle with area 3.14
    println!("{}", Rect { w: 2.0, h: 3.0 }.describe()); // shape with area 6.00
}

Unlike Go interfaces, Rust traits are implemented explicitly with impl Trait for Type. You can implement your trait for types you did not write, including i32 and Vec<T>. The orphan rule restricts this: either the trait or the type must be defined in your crate. That rule stops two crates from providing conflicting implementations.

16.2 Generic functions and trait bounds

use std::fmt::Display;

// "T can be any type that implements PartialOrd and Copy"
fn largest<T: PartialOrd + Copy>(items: &[T]) -> Option<T> {
    let mut it = items.iter().copied();
    let first = it.next()?;
    Some(it.fold(first, |best, x| if x > best { x } else { best }))
}

// `where` clauses read better when bounds get long.
fn report<K, V>(pairs: &[(K, V)]) -> String
where
    K: Display,
    V: Display + PartialOrd<i32>,
{
    pairs
        .iter()
        .map(|(k, v)| format!("{k}={v}{}", if *v > 100 { " (!)" } else { "" }))
        .collect::<Vec<_>>()
        .join(", ")
}

// `impl Trait` in argument position is shorthand for a generic parameter.
fn shout(msg: impl AsRef<str>) -> String { msg.as_ref().to_uppercase() }

fn main() {
    println!("{:?} {:?}", largest(&[3, 9, 2]), largest::<f64>(&[]));
    println!("{}", report(&[("latency_ms", 120), ("errors", 3)]));
    println!("{} {}", shout("hi"), shout(String::from("there")));
}

16.3 Monomorphization: what generics compile to

The compiler generates a separate copy of a generic function for every concrete type it is used with. largest::<i32> and largest::<f64> become two functions in the binary, each optimized for its type, with no indirection at runtime.

Rust generics Go generics C
Implementation monomorphization (one copy per type) GC-shape stenciling + dictionaries macros or void *
Runtime cost none; can be inlined sometimes an indirect call macros: none; void *: casts and no type safety
Compile time / binary size larger smaller —

This is what "zero-cost abstraction" means: the generic code runs as fast as code written by hand for each type. The cost moves to compile time and binary size. When that matters, you can switch to dyn Trait (Ch 17), which uses one copy of the code and dispatches through a vtable.

16.4 Associated types and generic traits

// A trait with an ASSOCIATED TYPE: each implementor picks exactly one `Output`.
trait Parser {
    type Output;
    fn parse(&self, input: &str) -> Option<Self::Output>;
}

struct PortParser;
impl Parser for PortParser {
    type Output = u16;
    fn parse(&self, input: &str) -> Option<u16> { input.parse().ok().filter(|p| *p != 0) }
}

struct CsvParser;
impl Parser for CsvParser {
    type Output = Vec<String>;
    fn parse(&self, input: &str) -> Option<Vec<String>> {
        Some(input.split(',').map(|s| s.trim().to_string()).collect())
    }
}

fn parse_all<P: Parser>(p: &P, inputs: &[&str]) -> Vec<Option<P::Output>> {
    inputs.iter().map(|i| p.parse(i)).collect()
}

fn main() {
    println!("{:?}", parse_all(&PortParser, &["80", "0", "x"]));  // [Some(80), None, None]
    println!("{:?}", parse_all(&CsvParser, &["a, b,c"]));
}

Use an associated type when each implementor has exactly one natural choice, as with Iterator::Item. Use a generic parameter (trait From<T>) when a type can implement the trait many times for different T.

16.5 Real-world example

std::io::Write is a trait. Files, sockets, Vec<u8>, Stdout, BufWriter<W>, TLS streams, and gzip encoders all implement it. Code written as fn write_report<W: Write>(out: &mut W) works with every one of them and gets monomorphized for each. This is Go's io.Writer pattern, with static dispatch instead of an interface value.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36