1. Introduction
Up to this point, we’ve lived in the world of single variables: a, b, x, name. That’s a great start, because your brain gets used to types, operations, and if/for. But after just a few problems, a new class of issues appears: you need to store not one value, but many.
For example: 7 daily expenses for the days of the week, 10 player attempts, 5 grades, a list of numbers down to zero, test results by question. A beginner’s first reaction is usually: “okay, I’ll just create a few variables.”
var d1, d2, d3, d4, d5, d6, d7 int
fmt.Scan(&d1, &d2, &d3, &d4, &d5, &d6, &d7)
And it seems fine—right up until the task gets slightly more interesting: “find the maximum,” “compute the average,” “print in reverse order,” “replace every third element,” “check whether values repeat.” With seven variables, you start writing code that looks like a 40-line accounting report: lots of repetition and almost no logic.
This is where the shift happens—from “variables as separate boxes” to the idea “I should have one box that contains many identical values.” And we already have the key to that door: we know for.
Imagine a “shelf” with numbered cells. You want to be able to say: “put a number into cell #3,” “read cell #5,” “loop over all cells.” That’s exactly what an array/slice is: data sits next to each other, and you access it by index.
The nicest part: indexing looks very natural and familiar from strings (we already accessed characters by index)—the same square brackets:
x := week[3] // take the 4th element
week[3] = 100 // change the 4th element
And now you can make a clean, “loop-based” input version: instead of seven separate variables—one container and one loop. First, just look at the idea (without focusing on the types yet) to feel the meaning:
var week [7]int
for i := 0; i < 7; i++ {
fmt.Scan(&week[i])
}
We read 7 numbers, and put each into the cell numbered i. It looks like you’re teaching the program to place values onto a shelf—and that’s exactly what we need when there’s lots of data.
Next in the lecture, we’ll cover two variants of this “shelf” in Go: an array (fixed size) and a slice (size can change). They look similar, but behave differently—and understanding that is an important step in growing up with Go.
2. Array [N]T: fixed length is part of the type
We agreed to think in terms of a “shelf”: there are many cells, each can be accessed by number, and a for loop can iterate through all of them. Now it’s important to choose which exact shelf this is in Go.
The first option is an array. It’s a shelf with a known number of cells in advance: exactly 7 days of the week, exactly 3 attempts, exactly 12 months. In Go, this is expressed directly in the type: the notation [N]T means “a container of N elements of type T.”
The key point that surprises you at first, and then starts to feel nice: the number N is not just a “size somewhere inside,” it’s part of the type. That’s why [3]int and [4]int are different types. The language forces you to be honest: if a function expects exactly 7 values, you can see it immediately—even before running the program.
Next we’ll look at how to create arrays, access elements by index, and what happens on assignment—this is where the array’s personality shows up: it’s a real “box of data” that gets copied as a whole.
An array in Go is a fixed-size “box.” And the size is not just a value—it’s part of the type. That is, [3]int and [4]int are two different types, like int and string. At first this may be a bit annoying (“why can’t I just…?”), but later you’ll understand that the language is pushing us to write more honest code.
Array syntax looks like this: [N]T, where N is the length and T is the element type. For example, [7]int is an array of seven integers.
Creating an array and indexing
Array indexing is standard: a[0], a[1], …, a[len(a)-1]. Indexes start at zero—yes, programmers chose this path, and now it’s too late to turn back.
package main
import "fmt"
func main() {
week := [3]int{10, 20, 30} // create and initialize an array of 3 elements
week[1] = 99
fmt.Println(week) // [10 99 30]
}
Here it’s important to feel one simple thing: an array stores elements inside itself, right “in the box.”
Copying an array on assignment
The most important property of an array: on assignment, all elements are copied. It’s like copying a file: you get an independent copy.
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a
b[0] = 99
fmt.Println("a:", a) // a: [1 2 3]
fmt.Println("b:", b) // b: [99 2 3]
}
b := a means: “create a new array b and copy all elements from a into it.” That’s why changing b does not affect a.
3. Slice []T: length is not in the type, and the behavior is different
A slice is Go’s main working tool for “lists.” It looks like an array, indexes like an array, iterates like an array… but internally it’s not a “box with elements,” it’s more like a “handle” that points to elements stored somewhere else. In very short terms: a slice is a view of part of an array, and different slices can view the same data.
At this point you usually want to ask: “Why so complicated?” The answer is pragmatic: because it provides flexibility and efficiency, but it requires discipline. And today we learn the basic discipline: understanding copying and passing to functions.
Slice literal and indexing
A slice is created with a literal []T{...}:
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
s[0] = 99
fmt.Println(s) // [99 2 3]
}
So far everything looks like an array. The difference will show up when we start assigning and passing values around.
Slice assignment: the “descriptor” is copied, not the elements
When you do t := s, Go does not copy all elements. It copies the “slice descriptor,” while the elements themselves remain shared (in general). That’s why changing an element through t is visible through s as well.
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
t := s
t[0] = 99
fmt.Println("s:", s) // s: [99 2 3]
fmt.Println("t:", t) // t: [99 2 3]
}
If an array is “copied the whole box,” then a slice is “copied the address label in the warehouse.” There are now two labels, but it’s the same warehouse.
4. Passing to a function: by value, but with different consequences
The phrase “in Go, everything is passed by value” sounds like a spell you’ll hear many times. It means: when calling a function, Go copies the argument value into the parameter.
And here comes the subtle point: what exactly is the “value” of an array, and what is the “value” of a slice?
For an array, the value is all of its elements. For a slice, the value is a small “descriptor structure” that points to elements stored elsewhere. So formally there is copying in both cases, but the meaning is different.
A contrasting example: the array “doesn’t change,” the slice “does”
package main
import "fmt"
func setFirstArray(a [3]int) {
a[0] = 100
}
func setFirstSlice(s []int) {
s[0] = 100
}
func main() {
a := [3]int{1, 2, 3}
s := []int{1, 2, 3}
setFirstArray(a)
setFirstSlice(s)
fmt.Println("array:", a) // array: [1 2 3]
fmt.Println("slice:", s) // slice: [100 2 3]
}
With an array, everything is straightforward: setFirstArray receives a copy of the array, you modify the copy, and the original remains unchanged.
With a slice it’s trickier: setFirstSlice receives a copy of the descriptor, but the descriptor points to the same elements, so changing s[0] modifies shared data—and you can see it “from the outside.”
5. Mini example: “Expense analyzer” and two strategies
To avoid studying collections in a vacuum, let’s continue our line of small console programs that compute something and print results. Imagine we have a mini app called an “expense analyzer”: we want to store expenses by day and compute the total.
There are two scenarios.
First scenario: we calculate expenses for exactly 7 days (a week). It’s convenient to express this with an array [7]int: the size is fixed, the data is always “complete.”
Second scenario: we calculate expenses for an arbitrary number of days (as many as the user enters). Here we need a slice []int.
Weekly expenses as an array [7]int
Let’s write a function to sum a week. Note: the parameter type is strictly [7]int.
package main
import "fmt"
func sumWeek(week [7]int) int {
total := 0
for _, v := range week {
total += v
}
return total
}
func main() {
week := [7]int{100, 0, 50, 20, 10, 0, 70}
fmt.Println(sumWeek(week)) // 250
}
There’s a very Go-style honesty here: the sumWeek function says, “I work only with a week of exactly 7 values.” No surprises.
But the cost is equally honest: when calling sumWeek(week), the array will be copied in full (though for 7 int values it’s not a big deal). Later, when the data gets larger, this will matter.
Arbitrary expenses as a slice []int
Now let’s make almost the same function, but for a slice:
package main
import "fmt"
func sumDays(days []int) int {
total := 0
for _, v := range days {
total += v
}
return total
}
func main() {
days := []int{100, 0, 50, 20}
fmt.Println(sumDays(days)) // 170
}
The sumDays function does not require a fixed length: any list length works.
And an important detail: passing days into the function copies only the “slice descriptor,” not all the elements. In most real programs, this is exactly why slices became Go’s “default collection.”
6. One more difference: comparison ==
At this stage it’s easy to fall into a trap: “Since a slice looks like an array, let’s compare two slices with ==.” And the compiler will politely (by compiler standards) say: “no.”
Go has a simple rule here.
Arrays can be compared with == if their elements are comparable.
Slices cannot be compared with == (except comparing to nil; we’ll cover what a nil slice is separately, calmly and without panic—although the word “panic” in Go usually shows up suddenly).
package main
import "fmt"
func main() {
a := [2]int{1, 2}
b := [2]int{1, 2}
fmt.Println(a == b) // true
s := []int{1, 2}
_ = s
fmt.Println(s == s) // compilation error: slices can't be compared like that
}
Why? In very human terms: a slice is not “the whole value,” but a “description of how to access data.” Comparing two descriptions for equality as “the data is the same” is too ambiguous (compare the address? the length? the elements? and what about huge slices?). So Go refuses to perform the comparison automatically and forces you to explicitly choose a comparison method.
7. Reinforcement: the table and the mental model
Sometimes it’s useful to become a “human table” for a minute—not for rote memorization, but so your brain sees the contrast.
Difference table
| Property | Array [N]T | Slice []T |
|---|---|---|
| Length | fixed and part of the type | stored in the value; the type does not contain the length |
| Assignment b := a | all elements are copied | the descriptor is copied; elements are usually shared |
| Passing to a function | the whole array is copied | the descriptor is copied; elements are usually shared |
| Comparison == | allowed (if elements are comparable) | not allowed (except comparison to nil) |
| Typical meaning | “exactly N elements” | “any number of elements” |
“Box” vs. “handle”
To make this settle not only in your head, but also in your hands, it helps to keep a simple picture. An array is a box with data. A slice is a handle that points to data (and there can be multiple handles).
flowchart LR
A["array [3]int"] -->|copying| B["array copy [3]int"]
A1["slice []int"] -->|copying| B1["slice copy []int"]
A1 --> D(("shared elements"))
B1 --> D
In the top part of the diagram, “copying” creates a new box. In the bottom part, it creates a second handle to the same elements.
8. Common mistakes
Mistake #1: treating [N]T and []T as “the same thing, just different brackets.”
That feeling is normal: the syntax is similar, indexing is similar, range is similar. But the types are different, and the copying semantics are different. A useful habit is to say out loud: “[7]int is a fixed-length week,” “[]int is a variable-length list.”
Mistake #2: expecting that t := s creates an independent copy of the slice elements.
This is a common “naive optimism” mistake: it feels like assignment means copying the data. With slices, only the descriptor is copied, so element changes are visible through both variables. If you truly need independence, you must explicitly copy the elements (but we won’t go further into that today—right now it’s important to understand why this effect happens at all).
Mistake #3: modifying an array in a function and being surprised that nothing changed outside.
With an array parameter, you are working with a copy. That’s not a bug, it’s the contract. If you want changes to be visible outside, you must either return the array from the function as a result, or use another approach (we’ll introduce such details gradually so we don’t turn the lecture into “everything about everything at once”).
Mistake #4: trying to compare two slices with == and arguing with the compiler.
The compiler almost always wins the argument, because it doesn’t get tired and it has no deadlines. If you need content equality, that’s a separate task and a separate explicitly chosen method. For now, it’s enough to remember the rule: arrays can be compared (under certain conditions), slices cannot.
Mistake #5: writing functions “for an array,” and then trying to feed them a slice.
[7]int and []int are different types. The function sumWeek([7]int) does not accept []int, even if the slice has exactly 7 elements. And this is again Go’s “honesty”: types must match, otherwise you would constantly be guessing what exactly the function author meant.
GO TO FULL VERSION