How does golang handle slices in variables?


(DefinitelyAHuman) #1

i originally posted this on reddit but i havent really been able to get the answers im looking for so im trying to branch out. I hope its not an issue linking to other questions on other sites.

I wrote a quick bit of code to go through the issue i have with slices and made separate cases for each problem. I’ll address them individually as they create some real inconsistencies in slice behaviour.

package main

import (
	"fmt"
)

func printfSlice(slc []int, itr int) {

	fmt.Printf("\ncount : %v", itr)

	fmt.Printf("\nslice01 val : %v"+
		"\nslice01 addr : %p "+
		"\nslice length : %v "+
		"\nslice01 cap : %v ",
		slc, &slc, len(slc), cap(slc))

	fmt.Printf("\n")
}

func main() {
	fmt.Println("start")

	slice01 := []int{2, 3, 5, 7, 11, 23}
	count01 := 0

	//count 1
	count01++
	printfSlice(slice01, count01)

	//count 2
	count01++
	slice01 = slice01[0:0]
	printfSlice(slice01, count01)

	//count 3
	count01++
	slice01 = slice01[1:5]
	printfSlice(slice01, count01)

	//count 4
	count01++
	slice01 = slice01[:3]
	printfSlice(slice01, count01)

	//count 5
	count01++
	slice01 = slice01[:]
	printfSlice(slice01, count01)

	//count 6
	count01++
	slice01 = slice01[0:cap(slice01)]
	printfSlice(slice01, count01)

	//count 7
	count01++
	slice01 = slice01[1:4]
	printfSlice(slice01, count01)

and the output

count : 1
slice01 val : [2 3 5 7 11 23]
slice01 addr : 0xc000046420 
slice length : 6 
slice01 cap : 6 

count : 2
slice01 val : []
slice01 addr : 0xc000046460 
slice length : 0 
slice01 cap : 6 

count : 3
slice01 val : [3 5 7 11]
slice01 addr : 0xc0000464a0 
slice length : 4 
slice01 cap : 5 

count : 4
slice01 val : [3 5 7]
slice01 addr : 0xc0000464e0 
slice length : 3 
slice01 cap : 5 

count : 5
slice01 val : [3 5 7]
slice01 addr : 0xc000046520 
slice length : 3 
slice01 cap : 5 

count : 6
slice01 val : [3 5 7 11 23]
slice01 addr : 0xc000046560 
slice length : 5 
slice01 cap : 5 

count : 7
slice01 val : [5 7 11]
slice01 addr : 0xc0000465a0 
slice length : 3 
slice01 cap : 4 
delve closed with code 0

a) pointers and addresses:

i understand that slices are pointers, but all the values end up with different addresses. if theyre all pointing back to the same array, shouldnt they all have the same address?

b) what is the actual underlying object everything is pointing to? :

in sect 2 of the output, i select the slice to length 0 by selecting nothing. then in sect 3, I select slice01[1:5] which gets me [3 5 7 11] , which makes no sense. I’ve defined slice01 as nothing in the previous section, so why wouldnt it return nothing or an error? It also now has reduced the capacity to 5. Why? it should just reduce the length not the capacity? why did making a specific selection in a slice reduce the capacity but selecting nothing didnt?

Then in sect 4 i select slice01 = slice01[:3] (remember im reusing using the SAME variable every single time). But instead of [2 3 5] from the original slice i get [3 5 7] from the slice defined in sect 3. This is even more confusing. In sect 3 it selected from sect 1 even though i redefined the variable in sect 2! and now in section 4 when i make another slice selection, it selects from sect 3 not sect 1? wtf is this?

sect 5, 6 and 7 continue to demonstrate the same problem.

what am i missing here? why do slices behave like this? is there some kind of pattern or rule that im missing?


(Ignacio Gómez) #2

Maybe this slightly modified version will clear it up for you: https://play.golang.org/p/7pnNMqUVlq0

I declared a [6]int array and sliced it to obtain the slice you started with, and then printed everything you printed, the underlying array’s address and the addresses of the first two elements (when present) too. As you can see, the slices change, but they are all views to the same underlying array. When you slice the array like s[:n] or s[0:n], you are keeping the view from the starting point and you can always regrow it back, but when you slice it like s[1:n], then your view starts at the position 1 of the underlying array, and thus effectively leaving the slice with a capacity of 5 in this case. As you can see, now the underlying array’s address of the new slice has moved to the element at position 1 of the original slice. As stated in https://blog.golang.org/go-slices-usage-and-internals:

Earlier we sliced s to a length shorter than its capacity. We can grow s to its capacity by slicing it again.

A slice cannot be grown beyond its capacity. Attempting to do so will cause a runtime panic, just as when indexing outside the bounds of a slice or array. Similarly, slices cannot be re-sliced below zero to access earlier elements in the array.

So there’s no way you are going back to position 0 of the original array by reslicing the slice that already started from 1, but you can grow them to anywhere within it’s capacity.

This happens again later when you reslice starting from a position other than 0 and further reduce the capacity. Of course, if you had kept an original slice, resliced it in another variable and then kept reslicing the latter, the former would be left untouched and yield your slice of length and capacity 6 pointing to the original array, as you can see in the last print (don’t mind the names, just kept your function).

Hope this clears things up.


(DefinitelyAHuman) #3

thanks for the reply. That really clears up the confusion about the capacity. I have a few more questions about the addresses though.

a)
in your adjusted script for the function you added

"\nunderlying array addr: %p\n",
slc, &slc, len(slc), cap(slc), slc)

and youre using slc as the under lying array. but isnt that the same as the line

"\nslice01 addr : %p "+

and yet you get 2 different addresses in the results. how?

slice01 val : [2 3 5 7 11 23]
slice01 addr : 0xc000046420
slice length : 6
slice01 cap : 6
underlying array addr: 0xc000072030
slc first element is at 0xc000072030 and second element is at 0xc000072038

if youre using “%p slc” in the function for both “underlying array addr” and “slice01 addr” , how are you getting 2 different address?

b)
you get the same memory address for the underlying array as you do for the first element. why? shouldn’t the elements have different addresses than the array itself?


(Ignacio Gómez) #4

Hi!

a) Yes, I’m using %p in both to print, but in the first one it’s &slcand in the second one it’s slc. They are not the same, as one is getting the address of the slice, i.e. printf("addr of the slice is %p, &slc), while the other gives the address of the array to which the slice points, i.e. printf("addr of the array is %p, slc)`, the difference being the &. As you may’ve seen in the answers at reddit and the docs, a slice is a pointer to an array, a length and a capacity, and the second print (printing the mentioned pointer) will give you the address of the underlying array, while the first print will give you a pointer to the slice, i.e., its address.

b) Well, an array is basically a contiguous collection of elements starting from some address. In this case, we have an array of ints, so if the array starts at 0xc000072030, that means it’s first element is at 0xc000072030, the second one is at 0xc000072038 (notice how in your latest example they differ in exactly 8 bytes, which is 64 bits, the size of an int in a 64 bits machine, while in the playground they differ in 4 bytes because the playground runs on a 32 bits architecture and thus an int is 32 bits sized), and so on. Go doesn’t have pointer arithmetics as C does where the relation between the first element and the array itself is more obvious, but under the hoods it’s mostly the same.

I’m being a bit simplistic here, but the point is that it makes sense that the first element of the array is effectively the array’s starting point, and given Go’s semantics where once you slice it past its initial member you can’t go back, then its new first element and thus its “new underlying array” (think it at what the slice’s starting point is now) is shifted to where you sliced it. On the other hand, being the slice just a pointer to the array and the latter still existing in memory, you are able to regrow the slice, but only till the limits of the array it was pointing to.


(DefinitelyAHuman) #5

okay that makes sense. thanks for the detailed response!