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:

  1. Mental overhead: Ownership, borrowing, lifetimes—concepts Go abstracts away
  2. Verbosity: More typing for simple operations
  3. Compiler fights: The borrow checker rejects valid (to me) code
  4. Learning curve: Go took me 2 weeks to feel productive; Rust feels like 2 months minimum

Pleasant Surprises:

  1. Error messages: Despite being strict, Rust’s errors are incredibly helpful
  2. No nil pointers: Option<T> prevents entire classes of bugs
  3. Explicit mutability: I can see at a glance what changes
  4. 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.