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.