Traits

Course 2 · Ch 2
Traits
Describing shared behavior across different types — Rust's answer to Go's interfaces

Course 1 built individual types with their own methods. This chapter introduces the tool for describing shared behavior across different types — a trait, directly comparable to Go's own interfaces (go2-2).

Defining and Implementing a Trait

trait Summary { fn summarize(&self) -> String; } impl Summary for Article { fn summarize(&self) -> String { format!("{} by {}", self.title, self.author) } } impl Summary for Tweet { fn summarize(&self) -> String { format!("@{}: {}", self.username, self.text) } }

A trait declares a method signature; a type opts in by writing its own impl Trait for Type block — Article and Tweet implement summarize completely differently.

Default Implementations

A trait method can include a default body — implementing types may use it as-is, or override it:

trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } impl Summary for Notice {} // empty impl — uses the default impl Summary for Article { fn summarize(&self) -> String { /* overrides the default */ } }

Traits as Parameters (impl Trait)

fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); }

notify accepts any type implementing Summary — the exact same "program to an interface, not an implementation" idea Go's interfaces exist for.

Trait Bounds

impl Trait is sugar for a more general, more powerful syntax — trait bounds — needed when a constraint can't be expressed with the sugar alone. Two parameters that must be the same concrete type is exactly that case:

fn compare<T: Summary>(a: &T, b: &T) -> bool { a.summarize() == b.summarize() } // Multiple bounds with +: fn notify_and_log<T: Summary + std::fmt::Display>(item: &T) { /* ... */ } // A where clause for readability when bounds get long: fn process<T, U>(t: &T, u: &U) where T: Summary, U: Clone, { /* ... */ }

impl Trait can't express "these two parameters must be the same type" — only a shared generic parameter with a trait bound can.

Contrast With Go's Interfaces

Go interfaces are satisfied implicitly — structural typing means if a type happens to have the right methods, it satisfies the interface automatically, with no explicit declaration anywhere. Rust traits are explicit: a type must write impl Trait for Type even if it already happens to have a method with a matching signature.

GoRust
Interface/trait satisfactionImplicit — structural typingExplicit — impl Trait for Type required
Accidental satisfactionPossible — matching methods are enoughImpossible — must be declared on purpose

Rust's explicitness prevents a type from accidentally satisfying a trait it was never intended to, at the cost of one extra impl block.

trait Definition

Declares method signatures a type can opt into implementing.

Default Implementation

A body a trait method already has; implementers may use it or override it.

impl Trait Parameter

Accept any type implementing a trait — sugar for the more general trait-bound syntax.

Trait Bound (<T: Trait>)

Needed when multiple parameters must share the exact same concrete type.

Default Implementations Let a Trait Evolve Safely
Adding a new method to a trait, with a default implementation, doesn't break any existing implementer — they simply inherit the default. Adding a new method with no default would instead be a compile error for every existing impl block, demanding they implement it too — the same "compiler tells you everywhere that needs updating" theme from Course 1's enum-variant gotcha, just showing up here as the case a default deliberately avoids.
The Orphan Rule: You Can't impl Just Anything for Anything
Rust requires that either the trait or the type being implemented is defined in your own crate — you can't write impl SomeExternalTrait for SomeExternalType when neither belongs to you. This exists specifically to prevent two different crates from providing conflicting implementations of the same trait for the same type, which would make it genuinely ambiguous which one applies. It's a real, sometimes-surprising restriction for newcomers coming from more permissive languages — but it's protecting a real guarantee (coherence), not an arbitrary limitation.

Coding Challenges

Challenge 1

Define a trait Describable with a method describe(&self) -> String that has a default implementation returning "No description available.". Implement it for a struct Book (overriding the default) and a struct Widget (using the default, via an empty impl block).

📄 View solution
Challenge 2

Write a function print_description(item: &impl Describable) using Challenge 1's trait, then explain why this same function could NOT be used to compare two Describable items of potentially different concrete types for equality, and what syntax change would be needed to require them to be the same type.

📄 View solution
Challenge 3

Explain, in your own words, why Go's implicit interface satisfaction could lead to a type accidentally satisfying an interface it wasn't designed for, and why Rust's explicit impl Trait for Type requirement makes that specific mistake impossible.

📄 View solution

Chapter 2 Quick Reference

  • trait Name { fn method(&self) -> T; } — declares shared behavior; types opt in via impl Trait for Type
  • Default implementations — a trait method can have a body; implementers may use it or override it
  • fn f(x: &impl Trait) — accepts any type implementing Trait; sugar for a trait bound
  • fn f<T: Trait>(a: &T, b: &T) — a trait bound; needed when parameters must share the same concrete type
  • T: TraitA + TraitB — multiple bounds; where clauses improve readability for longer bound lists
  • Go interfaces are satisfied implicitly (structural); Rust traits require an explicit impl block — no accidental satisfaction
  • Adding a defaulted trait method doesn't break existing implementers; adding one with no default does
  • The orphan rule: either the trait or the type must be local to your own crate
  • Next chapter: Generics — generic functions/structs, trait bounds on generics, and a contrast with Go's own generics