Go to Rust Series: ← No Garbage Collector | Series Overview | Mut vs Immutable →
The Concept Go Doesn’t Need
Lifetimes are Rust’s way of tracking how long references are valid-all at compile time.
Go doesn’t need lifetimes because the garbage collector tracks reference validity at runtime. Rust needs them because there’s no GC.
Go: References Just Work
Go:
func getFirst(a, b string) string {
if len(a) > len(b) {
return a
}
return b
}
func main() {
x := "short"
y := "longer"
result := getFirst(x, y)
fmt.Println(result)
}
No annotations needed. GC ensures result stays valid.
Rust: Compiler Needs Proof
Rust (won’t compile without lifetimes):
fn get_first(a: &str, b: &str) -> &str {
if a.len() > b.len() {
a
} else {
b
}
}
Error:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:37
|
1 | fn get_first(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
Rust needs to know: does the returned reference come from a or b? How long is it valid?
Fixed with lifetime annotation:
fn get_first<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() {
a
} else {
b
}
}
fn main() {
let x = "short";
let y = "longer";
let result = get_first(x, y);
println!("{}", result);
}
The 'a annotation says: “The returned reference lives as long as the shorter of a or b.”
What Are Lifetimes?
Lifetimes are compile-time annotations that tell the compiler how long references are valid.
Syntax:
'a- A lifetime parameter (pronounced “tick a”)&'a str- A reference with lifetime'a
Example:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
This says:
xandyboth have lifetime'a- The return value has lifetime
'a - Therefore: return value lives as long as both inputs
Lifetime Elision: When You Don’t Need Annotations
Rust has rules to infer lifetimes in simple cases:
Rule 1: Each parameter gets its own lifetime
Explicit:
fn print_str<'a>(s: &'a str) {
println!("{}", s);
}
Elided (compiler infers):
fn print_str(s: &str) {
println!("{}", s);
}
Rule 2: If one input lifetime, output gets same lifetime
Explicit:
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
Elided:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Rule 3: Multiple inputs, but one is &self, output gets &self lifetime
Explicit:
impl MyStruct {
fn get_data<'a>(&'a self) -> &'a str {
&self.data
}
}
Elided:
impl MyStruct {
fn get_data(&self) -> &str {
&self.data
}
}
When elision doesn’t work, you need explicit annotations.
Dangling References: Go vs Rust
Go: GC Saves You
Go:
func dangling() *int {
x := 42
return &x // OK! GC extends x's lifetime
}
func main() {
ptr := dangling()
fmt.Println(*ptr) // 42
}
GC realizes x is referenced and keeps it alive.
Rust: Compiler Rejects
Rust:
fn dangling() -> &i32 {
let x = 42;
&x // ERROR! x doesn't live long enough
}
Error:
error[E0106]: missing lifetime specifier
error[E0515]: cannot return reference to local variable `x`
Rust prevents the bug at compile time.
Fix: Return owned value
fn not_dangling() -> i32 {
42 // Return value, not reference
}
Structs with References
Go: No Annotations
Go:
type Excerpt struct {
text *string
}
func main() {
s := "Hello, world!"
e := Excerpt{text: &s}
fmt.Println(*e.text)
}
Works. GC tracks the reference.
Rust: Lifetime Annotations Required
Rust:
struct Excerpt<'a> {
text: &'a str,
}
fn main() {
let s = String::from("Hello, world!");
let e = Excerpt { text: &s };
println!("{}", e.text);
}
The 'a in Excerpt<'a> says: “This struct holds a reference that lives for 'a.”
What happens if the reference outlives the data?
fn main() {
let e;
{
let s = String::from("Hello");
e = Excerpt { text: &s };
} // s is dropped here
println!("{}", e.text); // ERROR!
}
Error:
error[E0597]: `s` does not live long enough
Rust prevents use-after-free at compile time.
Multiple Lifetimes
Sometimes you need different lifetimes for different parameters:
Rust:
fn announce<'a, 'b>(
announcement: &'a str,
context: &'b str
) -> &'a str {
println!("Context: {}", context);
announcement // Return announcement (lifetime 'a)
}
fn main() {
let ann = String::from("Important!");
{
let ctx = String::from("Some context");
let result = announce(&ann, &ctx);
println!("{}", result);
}
// ctx is dropped, but result (from ann) is still valid
}
This says:
announcementhas lifetime'acontexthas lifetime'b- Return value has lifetime
'a(tied to announcement, not context)
Static Lifetime: Lives Forever
Rust:
fn get_message() -> &'static str {
"This string literal lives for the entire program"
}
fn main() {
let msg = get_message();
println!("{}", msg);
}
'static means the reference is valid for the entire program duration (e.g., string literals, static variables).
Example with static variable:
static GLOBAL_MESSAGE: &str = "Global";
fn get_global() -> &'static str {
GLOBAL_MESSAGE
}
Real-World Example: Iterator
Go: No Lifetime Annotations
Go:
type Words struct {
text string
pos int
}
func (w *Words) Next() *string {
// Find next word and return reference
// GC handles lifetime
}
Rust: Lifetime Tied to Struct
Rust:
struct Words<'a> {
text: &'a str,
pos: usize,
}
impl<'a> Words<'a> {
fn new(text: &'a str) -> Self {
Words { text, pos: 0 }
}
fn next(&mut self) -> Option<&'a str> {
// Find next word
// Returned reference has same lifetime as original text
if self.pos >= self.text.len() {
return None;
}
let start = self.pos;
while self.pos < self.text.len() && !self.text.as_bytes()[self.pos].is_ascii_whitespace() {
self.pos += 1;
}
let word = &self.text[start..self.pos];
self.pos += 1; // Skip whitespace
Some(word)
}
}
fn main() {
let text = String::from("Hello Rust world");
let mut words = Words::new(&text);
while let Some(word) = words.next() {
println!("{}", word);
}
}
The 'a ensures returned words live as long as the original text.
Common Lifetime Patterns
Pattern 1: Returning One of Many References
Rust:
fn choose<'a>(first: &'a str, second: &'a str, condition: bool) -> &'a str {
if condition { first } else { second }
}
Return value tied to shortest lifetime of inputs.
Pattern 2: Struct Holding Multiple References
Rust:
struct Context<'a, 'b> {
name: &'a str,
description: &'b str,
}
impl<'a, 'b> Context<'a, 'b> {
fn print(&self) {
println!("{}: {}", self.name, self.description);
}
}
Different fields can have different lifetimes.
Pattern 3: Lifetime Bounds
Rust:
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: std::fmt::Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
Combining lifetimes with generics.
When Lifetimes Get Complex
Rust:
struct Parser<'a> {
input: &'a str,
}
impl<'a> Parser<'a> {
fn parse(&self) -> Result<Token<'a>, Error> {
// Token references data from input
// Token's lifetime tied to Parser's lifetime
unimplemented!()
}
}
struct Token<'a> {
value: &'a str,
}
This gets complex fast! But it ensures memory safety without GC.
The Mental Model
Go:
- “Pass references around”
- “GC will handle it”
Rust:
- “How long does this reference live?”
- “Does it outlive what it points to?”
- “Can the compiler prove safety?”
Why Go Developers Struggle
Coming from Go, lifetimes feel like unnecessary ceremony:
Go version (simple):
func process(data string) string {
return data
}
Rust version (explicit):
fn process<'a>(data: &'a str) -> &'a str {
data
}
But Rust’s explicitness prevents bugs:
Go (compiles, crashes at runtime):
func getRef() *string {
s := "temporary"
return &s // Looks safe, GC saves it
}
Rust (won’t compile):
fn get_ref() -> &str {
let s = "temporary".to_string();
&s // ERROR! s doesn't live long enough
}
Conclusion
Lifetimes in Rust:
- Ensure references never outlive their data
- Prevent dangling pointers at compile time
- Add complexity but eliminate runtime bugs
Go’s approach:
- No lifetime annotations needed
- GC handles everything at runtime
- Simpler to write, potential runtime issues
Trade-off:
- Go: Faster to write, runtime safety net
- Rust: More upfront work, compile-time guarantees
Next: Understanding mut vs immutable-why Rust makes you choose explicitly.
Go to Rust Series: ← No Garbage Collector | Series Overview | Mut vs Immutable →
Lifetimes verdict: Hardest concept for Go developers. But they prevent entire classes of bugs that GC can’t catch.