Phantom Types in Rust and Go: Type Safety Without Runtime Overhead
Imagine you’re running a package delivery service. Every package has a label — “Fragile”, “Perishable”, or “Standard” — that dictates how…
Imagine you’re running a package delivery service. Every package has a label — “Fragile”, “Perishable”, or “Standard” — that dictates how…
Phantom Types in Rust and Go: Type Safety Without Runtime Overhead
Imagine you’re running a package delivery service. Every package has a label — “Fragile”, “Perishable”, or “Standard” — that dictates how it should be handled.
Now imagine your delivery staff ignores these labels and just treats every box the same. What could go wrong?
- A fragile package might get tossed.
- A perishable item might be delivered late.
- Your customers might never trust you again.
This is exactly what happens in software when we ignore the intent of data.
This is where phantom types come in — like invisible labels enforced at compile time, so your code can’t misuse data even if it looks the same at runtime.
What Are Phantom Types?
Phantom types are type parameters that don’t exist at runtime but act as compile-time markers. They help you enforce rules and transitions in your code using the type system.
Think of them as stickers on your data boxes: not seen inside the box, but they tell you exactly how the box should be handled.
Rust’s Superpower: Zero-Cost Type Safety
Rust has a powerful type system that lets you use phantom types natively via PhantomData.
Rust Example: Package States
use std::marker::PhantomData;
struct Package<T> {
contents: String,
_marker: PhantomData<T>,
}
struct Fragile;
struct Perishable;
struct Standard;
impl Package<Fragile> {
fn handle_with_care(&self) {
println!("Handling fragile item: {}", self.contents);
}
}
impl Package<Perishable> {
fn refrigerate(&self) {
println!("Refrigerating: {}", self.contents);
}
}Usage:
let box1 = Package::<Fragile> {
contents: "Glassware".into(),
_marker: PhantomData,
};
box1.handle_with_care();
// box1.refrigerate(); // Compile errorGo Doesn’t Have Phantom Types… Or Does It?
Go doesn’t have PhantomData, but with the arrival of generics, we can mimic this pattern.
Go Version:
type Fragile struct{}
type Perishable struct{}
type Standard struct{}
type Package[T any] struct {
contents string
_ T
}
func HandleWithCare(p Package[Fragile]) {
fmt.Println("Handling fragile item:", p.contents)
}
func Refrigerate(p Package[Perishable]) {
fmt.Println("Refrigerating:", p.contents)
}Usage:
box := Package[Fragile]{contents: "Glassware"}
HandleWithCare(box)
// Refrigerate(box) // Compile error!Real Life Example: Bank Account Transaction Flow
Let’s build a safe bank transaction workflow:
- Created
- Verified
- Approved
- Executed
Rust:
use std::marker::PhantomData;
struct Created;
struct Verified;
struct Approved;
struct Executed;
struct Transaction<T> {
amount: u64,
_marker: PhantomData<T>,
}
impl Transaction<Created> {
fn verify(self) -> Transaction<Verified> {
println!("Verified transaction: {}", self.amount);
Transaction { amount: self.amount, _marker: PhantomData }
}
}
impl Transaction<Verified> {
fn approve(self) -> Transaction<Approved> {
println!("Approved transaction: {}", self.amount);
Transaction { amount: self.amount, _marker: PhantomData }
}
}
impl Transaction<Approved> {
fn execute(self) -> Transaction<Executed> {
println!("Executed transaction: {}", self.amount);
Transaction { amount: self.amount, _marker: PhantomData }
}
}Go:
type Created struct{}
type Verified struct{}
type Approved struct{}
type Executed struct{}
type Transaction[T any] struct {
Amount uint64
_ T
}
func Verify(tx Transaction[Created]) Transaction[Verified] {
fmt.Println("Verified transaction:", tx.Amount)
return Transaction[Verified]{Amount: tx.Amount}
}
func Approve(tx Transaction[Verified]) Transaction[Approved] {
fmt.Println("Approved transaction:", tx.Amount)
return Transaction[Approved]{Amount: tx.Amount}
}
func Execute(tx Transaction[Approved]) Transaction[Executed] {
fmt.Println("Executed transaction:", tx.Amount)
return Transaction[Executed]{Amount: tx.Amount}
}Why This Is Powerful
Rust vs Go: Comparing the Experience
Final Thoughts: Trust Your Type System
Phantom types might seem invisible, but their impact is huge. They turn “just data” into intent-driven, safely handled structures — like putting your variables on rails so they can’t fall off.
Whether you’re working in Rust or Go, using phantom types can dramatically improve safety, readability, and correctness of your code.