append to slice in Golang

287 阅读3分钟

Slice can be seen as dynamic array in Golang, we can append items to a slice using append function, which has the following syntax:

func append(slice []Type, elems ...Type) []Type

When I wrote code to find solutions for subsets of an integer array, the problem can be stated in detail,

Given an integer array, find all subsets of it.

and below is the code I wrote,

package main

import "fmt"

func recursion(nums []int, n int) [][]int {
	if n == 0 {
		res := [][]int{}
		res = append(res, []int{})
		return res
	}

	subsets := recursion(nums, n-1)
	new := [][]int{}
	for i := 0; i < len(subsets); i++ {
		new = append(new, append(subsets[i], nums[n-1]))
	}
	res := append(subsets, new...)
	return res
}

func subsets(nums []int) [][]int {
	if len(nums) == 0 {
		return nil
	}

	result := recursion(nums, len(nums))
	return result
}

func main() {
	nums := []int{1, 2, 3, 4, 5}
	fmt.Println(subsets(nums))
}

and the output,

[[] [1] [2] [1 2] [3] [1 3] [2 3] [1 2 3] [4] [1 4] [2 4] [1 2 4] [3 4] [1 3 4] [2 3 4] [1 2 3 5] [5] [1 5] [2 5] [1 2 5] [3 5] [1 3 5] [2 3 5] [1 2 3 5] [4 5] [1 4 5] [2 4 5] [1 2 4 5] [3 4 5] [1 3 4 5] [2 3 4 5] [1 2 3 5 5]]

Obviously, something is wrong with the code above. After some searching and debugging, I found the solution of the bug, when appending to a slice, the slice may be reallocated or not, decided by the number of items to be appended and the capacity of the slice already has. Let me illustrate this using the code below,

func main() {
	s := make([]int, 2)
	s = append(s, 1)
	fmt.Printf("address of slice: %p\n", &s[0])
	s = append(s, 2)
	fmt.Printf("address of slice: %p\n", &s[0])
	for i := 4; i < 100; i++ {
		s = append(s, i)
	}
	fmt.Printf("address of slice: %p\n", &s[0])
}

when appending 1 and 2 to slice, the slice capacity is big enough to store this two elements, so it does not need to be reallocated, the address of first element is the same before appending 2 and after appending 2,

address of slice: 0xc0000201c0
address of slice: 0xc0000201c0
address of slice: 0xc000108000

So when appending a 1d slice to a 2d slice dynamically, please notice that the underlying data address of 1d slice may be changed in another place, so in this case we need to allocate a new 1d slice to append to the 2d slice. After we changed new = append(new, append(subsets[i], nums[n-1])) to

old := make([]int, len(subsets[i]))
copy(old, subsets[i])
new = append(new, append(old, nums[n-1]))

the algorithm can work as expected now. When appending items to a slice, the returned slice may have same address as the original slice,

func main() {
	s := make([]int, 2)
	s = append(s, 1)
	fmt.Printf("address of s: %p\n", &s[0])
	s1 := append(s, 3)
	fmt.Printf("address of s1: %p, s:%p\n", &s1[0], &s[0])
}

As the slice s has a capacity of 2, when appending 3 to the slice, s does not need to be expended, so s1 and s has the same address,

address of s: 0xc0000b0020
address of s1: 0xc0000b0020, s:0xc0000b0020

but when appending more elements to the slice, the returned slice has a different address as the original slice s,

s := make([]int, 2)
s = append(s, 1)
fmt.Printf("address of s: %p\n", &s[0])
s1 := append(s, 3)
fmt.Printf("address of s1: %p, s:%p\n", &s1[0], &s[0])
s2 := append(s, 4, 5, 6, 7)
fmt.Printf("address of s1: %p, s2:%p, s:%p\n", &s1[0], &s2[0], &s[0])

output:

address of s: 0xc0000b0020
address of s1: 0xc0000b0020, s:0xc0000b0020
address of s1: 0xc0000b0020, s2:0xc0000ba040, s:0xc0000b0020

So after calling append(subsets[i], nums[n-1]), the returned slice may have the same underlying address as subsets[i], meaning many elements of 2d slice have the same address pointer, causing something wrong. What we expect is the elements of 2d slice have its own address without disturbing others when it is manipulated. So the snippet below can achieve that purpose.

old := make([]int, len(subsets[i]))
copy(old, subsets[i])
new = append(new, append(old, nums[n-1]))