Go to Rust Series: ← Lifetimes Explained | Series Overview | String vs &str →


The Default Difference

Go: Everything is mutable by default Rust: Everything is immutable by default

This single difference shapes how you write code in each language.

Go: Mutable by Default

Go:

func main() {
    x := 10
    x = 20      // No problem
    x = x + 5   // No problem
    fmt.Println(x)  // 25
}

Variables are mutable unless you use const (which is very limited):

const PI = 3.14  // Compile-time constant only
PI = 3.14159     // Error

Rust: Immutable by Default

Rust:

fn main() {
    let x = 10;
    x = 20;     // ERROR!
}

Error:

error[E0384]: cannot assign twice to immutable variable `x`

Fix: Use mut:

fn main() {
    let mut x = 10;
    x = 20;
    x = x + 5;
    println!("{}", x);  // 25
}

Why Immutable by Default?

Benefits:

  1. Explicit intent: When you see mut, you know it changes
  2. Thread safety: Immutable data can be shared safely
  3. Fewer bugs: Can’t accidentally modify data
  4. Optimization: Compiler can optimize immutable data better

Example:

fn calculate(value: i32) -> i32 {  // value is immutable
    // Can't do: value = value + 1
    value * 2  // Must return new value
}

Clear that calculate doesn’t modify its input.

Mutability in Structs

Go: All Fields Mutable

Go:

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    p.Name = "Bob"  // OK
    p.Age = 31      // OK
}

Rust: Struct Mutability is All-or-Nothing

Rust:

struct Person {
    name: String,
    age: u32,
}

fn main() {
    let p = Person {
        name: String::from("Alice"),
        age: 30,
    };

    p.name = String::from("Bob");  // ERROR!
    p.age = 31;                    // ERROR!
}

Fix: Make entire struct mutable:

fn main() {
    let mut p = Person {
        name: String::from("Alice"),
        age: 30,
    };

    p.name = String::from("Bob");  // OK
    p.age = 31;                    // OK
}

Caveat: Can’t have some fields mutable and others immutable on the same struct instance.

Interior Mutability: When You Need Flexibility

Sometimes you need to mutate data even when holding an immutable reference.

Go: No Special Pattern Needed

Go:

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

Rust: Cell and RefCell

Rust (Cell):

use std::cell::Cell;

struct Counter {
    count: Cell<i32>,
}

impl Counter {
    fn increment(&self) {  // Takes &self, not &mut self!
        let current = self.count.get();
        self.count.set(current + 1);
    }

    fn get(&self) -> i32 {
        self.count.get()
    }
}

fn main() {
    let counter = Counter {
        count: Cell::new(0),
    };

    counter.increment();
    counter.increment();
    println!("{}", counter.get());  // 2
}

Cell provides interior mutability for Copy types.

Rust (RefCell for non-Copy types):

use std::cell::RefCell;

struct Data {
    values: RefCell<Vec<i32>>,
}

impl Data {
    fn add(&self, value: i32) {  // &self, not &mut self
        self.values.borrow_mut().push(value);
    }

    fn get(&self) -> Vec<i32> {
        self.values.borrow().clone()
    }
}

fn main() {
    let data = Data {
        values: RefCell::new(vec![]),
    };

    data.add(1);
    data.add(2);
    println!("{:?}", data.get());  // [1, 2]
}

RefCell checks borrowing rules at runtime instead of compile time.

Function Parameters

Go: Mutable Pointers

Go:

func modify(nums []int) {
    nums[0] = 999  // Modifies original
}

func main() {
    data := []int{1, 2, 3}
    modify(data)
    fmt.Println(data)  // [999 2 3]
}

Slices pass the underlying array reference.

Rust: Explicit Mutable Borrows

Rust:

fn modify(nums: &mut Vec<i32>) {
    nums[0] = 999;
}

fn main() {
    let mut data = vec![1, 2, 3];
    modify(&mut data);
    println!("{:?}", data);  // [999, 2, 3]
}

&mut is explicit—you know the function can modify data.

Immutable borrow (can’t modify):

fn read(nums: &Vec<i32>) {
    println!("{:?}", nums);
    // Can't modify!
}

Shadowing vs Mutation

Go: Must Mutate

Go:

func main() {
    x := 5
    x = x + 1  // Mutation
    x = x * 2  // Mutation
    fmt.Println(x)  // 12
}

Rust: Can Shadow Instead

Rust:

fn main() {
    let x = 5;
    let x = x + 1;  // Shadowing (creates new binding)
    let x = x * 2;  // Shadowing again
    println!("{}", x);  // 12
}

Shadowing allows you to “change” a value without making it mutable.

Benefit: Can change type:

fn main() {
    let spaces = "   ";  // &str
    let spaces = spaces.len();  // usize
    println!("{}", spaces);  // 3
}

With mut, this doesn’t work:

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();  // ERROR! Type mismatch
}

Collections: Mutability Implications

Go: Collections Mutable by Default

Go:

func main() {
    nums := []int{1, 2, 3}
    nums = append(nums, 4)
    nums[0] = 999
    fmt.Println(nums)  // [999 2 3 4]
}

Rust: Must Declare Mutable

Rust:

fn main() {
    let nums = vec![1, 2, 3];
    nums.push(4);  // ERROR! nums is immutable
}

Fix:

fn main() {
    let mut nums = vec![1, 2, 3];
    nums.push(4);
    nums[0] = 999;
    println!("{:?}", nums);  // [999, 2, 3, 4]
}

Constants vs Immutable Variables

Go: const is Limited

Go:

const PI = 3.14  // OK
const NUMS = []int{1, 2, 3}  // ERROR! Only basic types allowed

var NUMS = []int{1, 2, 3}  // Must use var (mutable!)

Rust: const and Static

Rust:

const PI: f64 = 3.14;  // Compile-time constant
static NUMS: &[i32] = &[1, 2, 3];  // Static, lives forever

fn main() {
    println!("{}", PI);
    println!("{:?}", NUMS);
}

Or immutable variable:

fn main() {
    let nums = vec![1, 2, 3];  // Immutable, but stored on stack/heap
}

Thread Safety and Mutability

Go: Requires Manual Locking

Go:

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

Easy to forget locking → data races.

Rust: Enforced by Type System

Rust:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("{}", *counter.lock().unwrap());  // 10
}

Can’t forget locking—won’t compile without it!

Real-World Example: Configuration

Go: Mutable Config

Go:

type Config struct {
    Port int
    Host string
}

var GlobalConfig = Config{
    Port: 8080,
    Host: "localhost",
}

func updateConfig() {
    GlobalConfig.Port = 9000  // Mutation anywhere!
}

Easy to modify from anywhere → hard to track changes.

Rust: Immutable Config

Rust:

struct Config {
    port: u16,
    host: String,
}

static GLOBAL_CONFIG: Config = Config {  // ERROR! Can't have non-const in static
    port: 8080,
    host: String::from("localhost"),
};

Better pattern:

use std::sync::OnceLock;

struct Config {
    port: u16,
    host: String,
}

static CONFIG: OnceLock<Config> = OnceLock::new();

fn get_config() -> &'static Config {
    CONFIG.get_or_init(|| Config {
        port: 8080,
        host: String::from("localhost"),
    })
}

fn main() {
    let config = get_config();
    println!("{}", config.port);  // Can't mutate!
}

Configuration can’t be modified after initialization.

Method Receivers: &self vs &mut self

Go: Receiver Mutability

Go:

type Counter struct {
    value int
}

func (c *Counter) Increment() {  // Pointer receiver
    c.value++
}

func (c Counter) Get() int {  // Value receiver
    return c.value
}

Rust:

struct Counter {
    value: i32,
}

impl Counter {
    fn increment(&mut self) {  // Mutable borrow
        self.value += 1;
    }

    fn get(&self) -> i32 {  // Immutable borrow
        self.value
    }

    fn consume(self) -> i32 {  // Takes ownership
        self.value
    }
}

fn main() {
    let mut counter = Counter { value: 0 };
    counter.increment();
    println!("{}", counter.get());
}

Rust’s system is more explicit about mutation intent.

Conclusion

Aspect Go Rust
Default Mutable Immutable
Explicit mutation No Yes (mut)
Partial mutability N/A No (all-or-nothing)
Interior mutability Not needed Cell/RefCell
Thread safety Manual locks Enforced by type system
Const Limited Full featured

Go’s approach:

  • Simple: everything is mutable
  • Flexible: modify anything anywhere
  • Risk: Easy to introduce bugs

Rust’s approach:

  • Explicit: mutability must be declared
  • Safe: Compiler prevents concurrent mutation
  • Verbose: More typing upfront

The verdict: Rust’s immutability-by-default prevents bugs but requires more mental overhead. Go’s mutability is simpler but requires discipline.

Next: Understanding Rust’s confusing String vs &str types.


Go to Rust Series: ← Lifetimes Explained | Series Overview | String vs &str →


Mutability verdict: Rust’s explicit mutability prevents bugs. Go’s implicit mutability is simpler. Choose your trade-off.