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:
- Explicit intent: When you see
mut, you know it changes - Thread safety: Immutable data can be shared safely
- Fewer bugs: Can’t accidentally modify data
- 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.