Immutable vs Mutable Data Structures in Go

Siddharth Narayan
3 min readJan 22, 2025

--

When working with Go, understanding the distinction between immutable and mutable data structures can make a significant difference in how you design, debug, and scale your applications. While Go doesn’t enforce immutability at the language level, its idioms and design practices provide flexibility to use both paradigms effectively. In this article, we’ll explore these concepts in depth, along with practical examples and when to use each.

What Are Mutable and Immutable Data Structures?

Mutable Data Structures

A mutable data structure allows you to modify its content after creation. This is the default behavior in Go, as most data structures are designed with mutability in mind.

Examples of Mutable Data Structures

Slices: Slices in Go are backed by an array and can dynamically resize, allowing direct modification of their elements.

s := []int{1, 2, 3} s[0] = 10 // Modify the slice 
fmt.Println(s)
// Output: [10, 2, 3]

Maps: Maps provide a flexible way to store key-value pairs, allowing additions, updates, and deletions.

m := map[string]int{"a": 1, "b": 2} m["a"] = 42 // Update the value 
delete(m, "b") // Remove a key
fmt.Println(m) // Output: map[a:42]

Structs: Structs are mutable if accessed via a pointer, allowing you to modify their fields directly.

type Person struct {     Name string     Age  int }  
p := &Person{Name: "Alice", Age: 30}
p.Age = 31 // Modify the struct field fmt.Println(*p)
// Output: {Alice 31}

Benefits of Mutable Data Structures

  • Flexibility: Easy to update and modify data in place.
  • Performance: Avoids the overhead of creating new instances.

Challenges with Mutable Data

  • Concurrency Issues: Requires explicit synchronization when shared across goroutines.
  • Debugging Complexity: Mutations can lead to unexpected side effects.

Immutable Data Structures

Immutable data structures cannot be altered after they are created. While Go does not natively support immutability, it can be simulated using careful coding practices.

Examples of Immutable Data Structures

Strings: Strings in Go are inherently immutable. Any modification results in a new string being created.

s := "hello" s = "world" // Creates a new string 
fmt.Println(s) // Output: world

Custom Immutable Types: You can design immutable types by restricting access to internal fields.

type ImmutablePoint struct {     
x, y int
}
func NewImmutablePoint(x, y int) ImmutablePoint {
return ImmutablePoint{x: x, y: y}
}
func (p ImmutablePoint) X() int { return p.x }
func (p ImmutablePoint) Y() int { return p.y }

Functional Updates: Instead of modifying existing data, return a new version with updated fields.

type Point struct {     
X, Y int
}
func (p Point) Move(dx, dy int) Point {
return Point{X: p.X + dx, Y: p.Y + dy}
}

Benefits of Immutable Data Structures

  • Thread Safety: Eliminates the need for synchronization in concurrent environments.
  • Predictability: Reduces side effects, making code easier to reason about.

Challenges with Immutable Data

  • Performance Overhead: May require more memory and processing due to new object creation.
  • Limited Flexibility: Operations like appending to a list or updating fields require duplicating data.

When to Use Mutable vs Immutable

Choosing between mutable and immutable data structures depends on your use case:

Use Mutable Data Structures when:

  • Performance is critical, and you want to avoid creating multiple copies of data.
  • Data is not shared across goroutines or shared access is controlled.

Use Immutable Data Structures when:

  • You need thread-safe, side-effect-free operations.
  • Predictability and immutability improve code readability and debugging.

Concurrency and Safety

In concurrent programming, mutable data structures can lead to race conditions unless explicitly synchronized. Common synchronization methods in Go include:

  • sync.Mutex:
var mu sync.Mutex 
mu.Lock()
// Modify shared resource
mu.Unlock()
  • Channels: Channels provide a safe way to share data between goroutines.
ch := make(chan int) 
go func() { ch <- 42 }()
fmt.Println(<-ch)
// Output: 42

Immutable data structures inherently avoid these issues, making them ideal for concurrent scenarios.

--

--

Siddharth Narayan
Siddharth Narayan

Written by Siddharth Narayan

Software Engineer with Expertise in Go, Java, and Python | Problem Solver | Experienced in PostgreSQL, Data Structures, AWS, and Linux

No responses yet