Let’s start with the humble array. It’s the fundamental building block, the simplest collection type Go has, and frankly, it’s a bit of a diva. It demands to know its exact size at compile time and throws a fit if you even think about changing it. This rigidity is its greatest strength and its most annoying weakness.

An array isn’t just a reference to a sequence of values; it is the entire sequence. Think of it not as a pointer to a house, but as the entire, physical house itself. This has a crucial implication: assignment and passing to a function creates a full, deep copy of the entire data structure. This isn’t a “oh, I’ll just point to your data” situation. This is a “I’m renting a truck, moving every single one of your bricks to a new lot, and building an identical house” situation.

// The array type is defined by its size and element type: [n]T
// This is a house blueprint for a 3-room mansion of integers.
blueprint := [3]int{10, 20, 30} // [10 20 30]

// Let's build a new, independent house from the blueprint.
newHouse := blueprint // This copies every single value.

// Renovate the new house's first room. The original is untouched.
newHouse[0] = 999

fmt.Println(blueprint) // [10 20 30] <-- Original is fine.
fmt.Println(newHouse)  // [999 20 30] <-- Change is isolated.

The Type is In the Size

Here’s the first “quirky” design choice that bites newcomers: [3]int and [4]int are as related as int and string. They are completely distinct, incompatible types. You can’t assign one to the other, and you certainly can’t use them interchangeably as function arguments. The size is part of the type signature. This is why you rarely see arrays used directly in function signatures—it would be like writing a function that only accepts aquariums holding exactly 7 fish.

a := [3]int{1, 2, 3}
b := [4]int{1, 2, 3, 4}

// This will not compile: "cannot use a (type [3]int) as type [4]int"
// b = a

// This function has a very, very specific clientele.
func processExactThreeThings(things [3]int) {
    // ...
}
// processExactThreeThings(b) // Also won't compile.

Passing Arrays is a Performance Trap

Because of this copy-by-value behavior, passing a large array to a function is a phenomenally bad idea. You’re not just passing a little memory address; you’re forcing the runtime to duplicate the entire chunk of memory on the stack. If you have a [1_000_000]int and you pass it around, congratulations, you’re now copying 8 MB of data every single time. The runtime will hate you, and your CPU cycles will weep.

var bigArray [1e6]int // 1,000,000 integers. Allocates ~8MB on the stack.

func processArray(data [1e6]int) { // Yikes. This copies the entire 8MB.
    data[0] = 1 // This change is lost when the function returns.
}
processArray(bigArray) // This call is brutally expensive.
fmt.Println(bigArray[0]) // Still 0. The function worked on a copy.

So Why Do Arrays Even Exist?

If they’re so inflexible and expensive, what’s the point? Two reasons:

  1. They are the backbone of slices. A slice is just a fancy struct that points to an underlying array. Without arrays, slices have nothing to manage. They’re the predictable, contiguous block of memory that makes slices fast.
  2. Predictability and Compile-Time Safety. When you absolutely need a fixed-length collection and want to guarantee that length at compile time, an array is your tool. Think of transformation matrices ([4][4]float64), fixed-size ciphers, or any domain where the size is a fundamental part of the data’s meaning. They are the precision instrument, not the duct tape.

The best practice is clear: Use arrays for fixed-size, compile-time-known structures, especially small ones, where value semantics are desirable. For virtually everything else—and I mean 99.9% of cases—you want a slice. The array is the foundation, but the slice is the house you actually live in.