Go DOES NOT have references!

As a beginner, I found it a bit confusing to work with slices in Go. Even now, I sometimes forget this. When reviewing code, I often see that colleagues pass pointers to slices in an attempt to optimize performance. Or people might be hesitant to slice a slice because they’re concerned it could affect performance. Maps are even more confusing! I wanted to summarize a few design decisions in Go that differ from those in both C and Java.

  1. Every time you pass a variable, its value is copied. This is also true when you assign a variable’s value to another variable.
type integer struct {
	val int
}

func f(i integer) {
	i.val *= 2
}

func main() {
	a := integer{val: 1}
	f(a)
	fmt.Println(a.val) // prints 1
	b := a
	b.val = 3
	fmt.Println(a.val, b.val) // prints 1 3
}

2. Go does not have reference variables. It does have pointers, but you cannot perform arithmetic operations on the addresses stored in pointers.

3. Arrays in Go have a fixed size, which is part of their type definition. For example, you cannot pass an array of type [5]int to a function that expects a [6]int because they are considered different types.

The statement b := a declares a new variable, b, of type [5]int, and copies the contents of a into b. Updating b will not affect a because a and b are independent values.

func main() {
        var a [5]int
        b := a
        b[2] = 7
        fmt.Println(a, b) // prints [0 0 0 0 0] [0 0 7 0 0]
}

4. Slices are an abstraction over arrays that make them more flexible and easier to use. Unlike arrays, slices do not require a fixed size when they are initialized. You can find the definition of slices in runtime/slice.go, where they are shown to contain a pointer to an underlying array.

type slice struct {
	array unsafe.Pointer
 	len   int
	cap   int
}

5. When you pass a slice to a function, it does not copy the underlying array. Instead, it creates a new slice object that points to the same underlying array and copies the slice’s metadata (such as length and capacity). As a result, if you modify the contents of the slice within the function, the changes will affect the original slice as well.

func f(nums []int) {
	nums[1] = 4
}

func main() {
	a := []int{1, 2, 3}
	f(a)
	fmt.Println(a) // prints [1 4 3]
}

6. When you append to a slice and assign the result back to the original slice variable, the original slice may change its underlying array. This means that changes to the new slice won’t affect the original slice.

func f(nums []int) {
	nums = append(nums, 4)
}

func main() {
	a := []int{1, 2, 3}
	f(a)
	fmt.Println(a) // prints [1 2 3]
}

7. Unlike slices, maps are not structs. A map is essentially a pointer to memory managed by the runtime. This means that if you modify a map inside a function, the changes will affect the original map as well.

func f(m map[int]bool) {
	m[0] = true
}

func main() {
	m := make(map[int]bool)
	f(m)
	fmt.Println(m) // prints map[0:true]
}

Read more:

1. Slices from the ground up
2. Go Slices: usage and internals
3. Go Data Structures