How to compare Go slices?
February 11, 2022
Problem: You have two slices that potentially share the same memory buffer meaning that appending to one can cause changes in another one. So, how to understand if two slices share memory?
Let’s imagine we have a functions like:
func IsMemoryShared(a, b []int) bool {
...
}
And we want to preserve following behaviour:
a := []{1, 2, 3}
b := []{1, 2, 3}
IsMemoryShared(a, a) // true
IsMemoryShared(a[:2]) // true
IsMemoryShared(a, b) // false
IsMemoryShared(a[1:0], b[1:0]) // false
It is known that slice in Go backed by a struct and contain a pointer to a place in memory where slice data is stored and two integers - length and capacity of the slice.
Technically you can access the array
pointer by dereferencing slice value (may not work in future releases if slice
struct will be changed, for example, order of properties will be changed or compiler will decide to reorder struct fields). Playground
a := []int{1, 2, 3}
fmt.Println((unsafe.Pointer)(&a)) // 0xc00000c030
But just accessing array
address doesn’t solve the problem cause for “sliced” slice you will get different address, check here:
a := []int{1, 2, 3}
b := a[1:]
fmt.Println((unsafe.Pointer)(&a)) // 0xc00010a000
fmt.Println((unsafe.Pointer)(&b)) // 0xc00010a018
There is a nice trick that becomes possible cause slices in Go are stored as pure arrays in memory which can’t be “resized” but actually got copied when slice needs to grow. So we can technically check the address of the last element in the slice and compare it, let’s try it out:
a := []int{1, 2, 3}
b := a[1:]
fmt.Println((unsafe.Pointer)(&a[len(a)-1])) // 0xc0000be010
fmt.Println((unsafe.Pointer)(&b[len(b)-1])) // 0xc0000be010
But that is not all, in case if slice is cutted from the end it will not work:
a := []int{1, 2, 3}
b := a[:1]
fmt.Println((unsafe.Pointer)(&a[len(a)-1])) // 0xc000016028
fmt.Println((unsafe.Pointer)(&b[len(b)-1])) // 0xc000016018
The solution is to take the address of the last element in the backed array and we know that this array size is defined by a capacity of a slice, see here:
a := []int{1, 2, 3}
b := a[1:2]
addr := func(v []int) unsafe.Pointer {
v2 := v[:cap(v)]
return (unsafe.Pointer)(&v2[len(v2)-1])
}
fmt.Println(a, addr(a)) // [1 2 3] 0xc000126008
fmt.Println(b, addr(b)) // [2] 0xc000126008
So basically algorithm is the following:
- resize your slice to make
len(a) == cap(a)
(that changes onlylen
parameter and does not result in allocating of new memory) - get the latest element of the resulting slice
- compare the address of the found element
Even if solution looks solid, unfortunatelly, it is not. In Go
there is way to change slice capacity down without reallocation:
a := []int{1, 2, 3}
b := a[1:2:2] // <- slice operator can have 3 values
addr := func(v []int) unsafe.Pointer {
v2 := v[:cap(v)]
return (unsafe.Pointer)(&v2[len(v2)-1])
}
fmt.Println(a, addr(a)) // [1 2 3] 0xc0000b8010
fmt.Println(b, addr(b)) // [2] 0xc0000b8008
Slice operator actually has two forms, one is common - [start:len]
and second one is quite rare - [start:len:cap]
. You can’t set a capacity greater than the actual size of the backing memory array but you can change it down. And for such cases there is no easy way to compare slices, one thing that you can do is to check if slices overlap by checking if two arrays overlap but it will also not guarantee the correct result cause two slices can share the same memory but not overlap.