Go to Rust Series: Series Overview | Cargo vs Go Modules →
The Setup
After years of writing Go, I decided to spend a few months learning Rust. Everyone kept talking about “fearless concurrency,” “zero-cost abstractions,” and “memory safety without garbage collection.” As a pragmatic developer, I wanted to see what the fuss was about.
Day 1 was… humbling.
First Shock: Everything Needs Annotations
Coming from Go, where types are inferred elegantly, Rust feels verbose. Here’s my first attempt at a simple function:
Go (what I’m used to):
package main
import "fmt"
func greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func main() {
message := greet("World")
fmt.Println(message)
}
Clean, readable, straightforward. Now Rust:
Rust (my first attempt):
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
let message = greet("World");
println!("{}", message);
}
Wait, what’s &str? Why not just str? And why is format! a macro with that ! symbol? And why do I need {} in the string instead of just using the variable name?
Second Shock: The Compiler is… Mean?
In Go, if I forget to use a variable, the compiler politely tells me:
./main.go:7:2: imported and not used: "fmt"
In Rust, I tried to write this:
fn main() {
let name = String::from("Gopher");
let name_ref = &name;
let name_ref2 = &name;
println!("{}", name); // Try to use original
}
And got:
error[E0502]: cannot borrow `name` as immutable because it is also borrowed as mutable
Wait, I didn’t even borrow it as mutable! This is my first encounter with the borrow checker. In Go, I could pass name around freely. In Rust, every reference has rules.
Third Shock: No Nil, But Also… Complicated?
In Go, checking for nil is straightforward:
Go:
func findUser(id int) *User {
if id == 0 {
return nil
}
return &User{ID: id, Name: "Test"}
}
func main() {
user := findUser(0)
if user != nil {
fmt.Println(user.Name)
}
}
In Rust, there’s no nil or null. Instead, there’s Option<T>:
Rust:
struct User {
id: u32,
name: String,
}
fn find_user(id: u32) -> Option<User> {
if id == 0 {
None
} else {
Some(User {
id,
name: String::from("Test")
})
}
}
fn main() {
let user = find_user(0);
match user {
Some(u) => println!("{}", u.name),
None => println!("User not found"),
}
}
Okay, this is more explicit. No accidental nil pointer dereferences. But it’s also more typing. Is the safety worth it?
Fourth Shock: Ownership Everywhere
This blew my mind. In Go, I can do this without thinking:
Go:
package main
import "fmt"
func printAndReturn(s string) string {
fmt.Println(s)
return s // No problem!
}
func main() {
msg := "Hello"
msg2 := printAndReturn(msg)
fmt.Println(msg) // Still works!
fmt.Println(msg2) // Still works!
}
My first Rust attempt:
Rust (WRONG):
fn print_and_return(s: String) -> String {
println!("{}", s);
s // This moves ownership back
}
fn main() {
let msg = String::from("Hello");
let msg2 = print_and_return(msg); // msg is MOVED here
println!("{}", msg); // ERROR! msg is moved!
println!("{}", msg2); // This works
}
Compiler error:
error[E0382]: borrow of moved value: `msg`
--> src/main.rs:9:20
|
7 | let msg = String::from("Hello");
| --- move occurs because `msg` has type `String`
8 | let msg2 = print_and_return(msg);
| --- value moved here
9 | println!("{}", msg);
| ^^^ value borrowed here after move
Wait, what? The value moved? In Go, strings are copied implicitly. In Rust, they’re moved unless you explicitly clone or borrow.
The correct way:
Rust (CORRECT - using references):
fn print_and_return(s: &str) -> String {
println!("{}", s);
s.to_string()
}
fn main() {
let msg = String::from("Hello");
let msg2 = print_and_return(&msg); // Borrow instead of move
println!("{}", msg); // Works!
println!("{}", msg2); // Works!
}
Or if you want to keep it simple:
Rust (CORRECT - explicit clone):
fn print_and_return(s: String) -> String {
println!("{}", s);
s.clone() // Explicit copy
}
fn main() {
let msg = String::from("Hello");
let msg2 = print_and_return(msg.clone()); // Clone before passing
println!("{}", msg); // Works!
println!("{}", msg2); // Works!
}
But now I’m cloning everywhere. Is this expensive? When should I clone vs borrow? This is mental overhead Go never had.
Fifth Shock: Mutability is Opt-In
In Go, everything is mutable by default:
Go:
func main() {
count := 0
count = count + 1 // Fine
count = count + 1 // Fine
fmt.Println(count) // 2
}
In Rust, you must declare mutability:
Rust:
fn main() {
let count = 0;
count = count + 1; // ERROR!
}
Compiler says:
error[E0384]: cannot assign twice to immutable variable `count`
You need mut:
Rust (CORRECT):
fn main() {
let mut count = 0;
count = count + 1; // Fine
count = count + 1; // Fine
println!("{}", count); // 2
}
At first, this felt like unnecessary ceremony. But I could see the benefit: you know exactly what can change.
The “Aha!” Moment (Sort Of)
After struggling for hours, I had a small victory. I wrote a simple Vec (Rust’s dynamic array) function:
Rust:
fn sum_numbers(numbers: &[i32]) -> i32 {
let mut total = 0;
for num in numbers {
total += num;
}
total
}
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let result = sum_numbers(&nums);
println!("Sum: {}", result);
println!("Original: {:?}", nums); // nums still usable!
}
This worked! I borrowed the slice, iterated, and returned a value. The original vector was still usable because I didn’t move it.
Compare to Go:
Go:
func sumNumbers(numbers []int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
nums := []int{1, 2, 3, 4, 5}
result := sumNumbers(nums)
fmt.Printf("Sum: %d\n", result)
fmt.Printf("Original: %v\n", nums)
}
The Go version is simpler, but Rust’s version guarantees sum_numbers can’t modify the original slice (because it’s a shared reference &).
Error Handling: Verbose but Explicit
Go’s error handling:
Go:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
Rust’s equivalent:
Rust:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}
Or with the ? operator (which I learned later):
Rust (cleaner):
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
return Err(String::from("division by zero"));
}
Ok(a / b)
}
fn main() -> Result<(), String> {
let result = divide(10.0, 2.0)?; // Propagates error automatically
println!("Result: {}", result);
Ok(())
}
The ? operator is like Go’s if err != nil { return err } but built into the language. Interesting!
Day 1 Feelings
Frustrations:
- Mental overhead: Ownership, borrowing, lifetimes—concepts Go abstracts away
- Verbosity: More typing for simple operations
- Compiler fights: The borrow checker rejects valid (to me) code
- Learning curve: Go took me 2 weeks to feel productive; Rust feels like 2 months minimum
Pleasant Surprises:
- Error messages: Despite being strict, Rust’s errors are incredibly helpful
- No nil pointers:
Option<T>prevents entire classes of bugs - Explicit mutability: I can see at a glance what changes
- Cargo: The build tool is excellent (more on this in next post)
First Day Conclusions
After 8 hours with Rust, I wrote maybe 100 lines of working code (compared to 1000+ lines I’d write in Go). But I learned:
- Rust makes you think about memory upfront instead of letting GC handle it
- The compiler is strict but helpful—it’s teaching me, not punishing me
- Explicitness has a cost—but prevents runtime surprises
- This will take time—and that’s okay
Would I recommend Rust on Day 1? Not if you want to ship quickly. But if you want to understand memory safety deeply and write code that won’t crash at 3 AM with a nil pointer panic? Keep reading.
Quick Comparison: Go vs Rust (Day 1 Edition)
| Feature | Go | Rust |
|---|---|---|
| Learning curve | Gentle | Steep |
| Time to first working program | 30 minutes | 3 hours (with errors) |
| Error messages | Brief | Detailed & helpful |
| Memory management | GC (automatic) | Ownership (manual) |
| Nil/null safety | Runtime checks | Compile-time prevention |
| Mutability | Default | Explicit opt-in |
| String handling | One type | String vs &str confusion |
What’s Next?
Tomorrow I’m diving into Cargo vs Go Modules. Spoiler: Cargo is actually really good.
Then I’ll tackle why Hello World in Rust teaches you more than Go’s version, and why the Rust compiler feels like a strict teacher who actually cares about your learning.
Stay tuned if you want to follow along with the struggles!
Go to Rust Series: Series Overview | Cargo vs Go Modules →
Day 1 verdict: Humbled but intrigued. The compiler won most fights, but I’m learning why it’s so strict.