Golang - Data Structures - Mutability in Arrays vs. Slices

Publish date: 2025-09-10
Tags: <a href="https://programmercave.com/tags/Go/">Go</a>

Introduction

Both arrays and slices in Go allow you to change their elements after creation—this is what we mean by “mutability.” But how changes affect other parts of your code differs a lot between the two. This note builds on the basics of arrays and slices by focusing on mutability in practice, including how to control it safely in larger programs. We’ll cover advanced tips useful for experienced developers, like handling shared changes in concurrent code or designing functions that avoid surprises.


The Core of Mutability: Changes and Sharing

Analogy: Updating an array is like editing a personal notebook—your changes stay in your copy. Updating a slice is like editing a shared document—everyone with access sees the changes instantly, unless someone makes a private copy.


Controlling Slice Mutability with Capacity Limits

Slices can share storage, but you can limit this sharing by controlling how much room a slice has to grow without creating new storage.

Code Example: Limiting Shared Changes

package main

import "fmt"

func main() {
    parent := []int{10, 20, 30, 40, 50}

    // Unlimited growth space: shares with parent
    unlimited := parent[1:3] // [20 30], can grow into parent's space

    // Limited growth space: forces new storage on growth
    limited := parent[1:3:3] // [20 30], max=3 so capacity=2 (can't grow without new storage)

    unlimited = append(unlimited, 99) // Might change parent
    limited = append(limited, 99)     // Always creates new storage, safe from changing parent

    fmt.Println("Parent after unlimited append:", parent) // Could be changed
    fmt.Println("Parent after limited append:", parent)   // Unchanged
}

Output (may vary, but shows the idea):

Parent after unlimited append: [10 20 30 99 50] // Shared change happened
Parent after limited append: [10 20 30 99 50]   // No further change

This technique is great for handing out “read-only views” that can’t accidentally mutate the original through growth.


Safe Copying to Break Mutability Sharing

To make sure changes don’t affect the original, create a full copy with its own storage.

Code Example: Cloning for Independent Changes

package main

import (
    "fmt"
    "slices" // Need this for slices.Clone
)

func main() {
    original := []string{"apple", "banana", "cherry"}

    cloned := slices.Clone(original) // New storage, safe to change

    cloned[0] = "avocado" // Only affects cloned

    fmt.Println("Original:", original) // Unchanged
    fmt.Println("Cloned:", cloned)     // Updated
}

Output:

Original: [apple banana cherry]
Cloned: [avocado banana cherry]

For older Go versions, use append([]T(nil), original...) instead—it’s the same idea but less readable.


Mutability in Concurrent Code

When multiple parts of your program run at the same time (using goroutines), mutability can cause “data races”—unpredictable bugs from unsynchronized changes.

Analogy: Think of concurrent updates like multiple chefs chopping veggies on separate cutting boards (different positions). It’s fine until someone rearranges the boards mid-chop.

Code Example: Concurrent Updates Without Races

package main

import (
    "fmt"
    "sync"
)

func main() {
    s := make([]int, 5) // Pre-allocate to avoid growth
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            s[idx] = idx * 10 // Update own position safely
        }(i)
    }

    wg.Wait()
    fmt.Println(s) // Output like [0 10 20 30 40] (order may vary but correct)
}

Run with go run -race main.go to check for races—none here because positions are separate and no size changes.


Mutability in Methods and Functions

How you define methods affects whether changes stick to the original data.

This is key for designing types that wrap arrays or slices, ensuring changes behave as expected.


Interview Question and Answer

Interview Question: “How do you prevent a sub-slice from mutating the parent’s data during append?”

Answer: “Use the full slice expression like parent[low:high:max] to limit capacity, forcing append to allocate new storage. Or clone the sub-slice with slices.Clone for full independence.”

This note adds practical ways to manage mutability without repeating basics, helping you build reliable code in teams or high-performance systems. For 7+ years engineers: Remember, mutability control is about ownership—decide early if data should be shared or copied to avoid debugging nightmares in production.

Tags: <a href="https://programmercave.com/tags/Go/">Go</a>