Why slicing to zero length doesn't drop elements?

Hi guys

I am recapping some code to refresh my knowledge and I found the array example in Go Tour, but I don’t understand why reducing the size by setting and array = array[:0] does not drop elements.

The following code outputs:

len=5 cap=5 [1 2 3 4 5] //the original array
len=0 cap=5 [] //array gets reduced
len=5 cap=5 [1 2 3 4 5] /// now the we can go back to original array (elements were still there)
len=3 cap=3 [3 4 5] // if we use [N:] elements get dropped from array

So why does s[:0] does not drop elements and behave differently compared to s[N:] ?
Is this by design?

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4, 5}
	printSlice(s)

	// Slice the slice to give it zero length. 
	// Elements are not dropped
	s = s[:0]
	printSlice(s)

	// extend array to original size, 
	// all elements are still there
	s = s[:5]
	printSlice(s)

	// Drop its first two values.
	// but why does it drop elements and we can't go back to the original?
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

In the Golang blog there is entry that discuss this.

Go Slices: usage and internals (Go Slices: usage and internals - The Go Programming Language)

Search for ‘A possible “gotcha”’ where :
As mentioned earlier, re-slicing a slice doesn’t make a copy of the underlying array. The full array will be kept in memory until it is no longer referenced. Occasionally this can cause the program to hold all the data in memory when only a small piece of it is needed.

Go slices consist of 3 fields:

  1. A pointer to the first element in the slice
  2. The length of the slice
  3. The capacity of the slice’s underlying memory

When you reslice a slice to a smaller length, the pointer to the first element and the capacity remain the same, but the length decreases (in the case of slice[:0], the length is decreased to 0 but the other 2 fields stay the same).

When you reslice from the front of the slice, something else happens: The pointer to the first element is set to the pointer to the nth element that you’re slicing from (i.e. slice[2:] means to change the pointer from pointing to &slice[0] to &slice[2]). Then the length and capacities decrease by, in this case, 2:

memory:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

s:
      +-+-+-+-+-+
      | | | | | |
      +-+-+-+-+-+
      ^         ^
      |         |
      +- data   |
         len: 5-+
         cap: 5-+

s[:0]
      +-+-+-+-+-+
      | | | | | |
      +-+-+-+-+-+
      ^         ^
      |         |
      +- data   |
      +- len: 0 |
         cap: 5-+

s[2:]
          +-+-+-+
          | | | |
          +-+-+-+
          ^     ^
          |     +-+
          +- data |
             len: 3
             cap: 3

While it’s technically possible to use the unsafe and reflect packages to manipulate the slice data to “rewind” the slice from s[2:] back to s[0:], in the general case where your slices are coming from who knows where, that’s dangerous to do because you don’t know if the slice you’re getting passed into your function was sliced from s[2:] or just plain ol’ s.

2 Likes

HI @juniormayhe,

In addition to the previous answers, this article might help wrapping one’s head around the relation between slices and arrays and the resulting behaviors.

(Side note: ignore the “how to run the code” section at the end, it’s obviously from the pre-Go-Modules era.)

1 Like

Hey, I followed up on those examples about Spliting arrays and resulting slices and now I can see things I didn’t notice previously. Sometimes the slice may point to the same memory address of the underlying array or a completely new memory address, depending on certain conditions. I guess the slices might cause a lot of trouble if we are not aware of the function behavior that returns a slice. Maybe we can try to avoid slice issues by creating tests to confirm the expected behavior. Thanks, Christoph and everyone. :love_you_gesture:

1 Like

I tried to address this problem by first assigning nil to the slice
and then making changes freshly

	s = nil
	printSlice(s)

	// s = s[:3]
	// panic: runtime error: slice bounds out of range [:3] with capacity 0

	s = []int{9, 8, 7, 6, 5}
	printSlice(s) // NO More leakage

	s = s[:5]
	printSlice(s)

Is it correct?

I don’t get what you mean with leakage in this context.

What I got in my tests is that when we slice a populated array with the low limit as in s[2:] we get what I believe to be a brand new underlining array in a new memory address. So we cannot go back (rewind) because there are no elements in positions 0 and 1.

package main

import "fmt"

func main() {
    // populate array
    s := []int{1, 2, 3}
    printSlice("s   | ", s)

    // Slice the array to give it zero length.
    // Elements are not dropped. Still have the same memory address to underlying array
    s = s[:0]
    printSlice("s[:0]   | ", s)

    // extend array to original size,
    // all elements are still there in the same memory address of underlying array
    s = s[:3]
    printSlice("s[:3]   | ", s)

    // when skipping first two values by setting low limit to 2,
    // a new array is created in memory with a different len and capacity
    s = s[2:] // the underlying array is replaced with a new address
    printSlice("s[2:]   | ", s)
}

func printSlice(m string, s []int) {
    fmt.Printf("%v len=%d cap=%d address=%p value=%v\n", m, len(s), cap(s), s, s)
}

Output

s       |  len=3 cap=3 address=0xc00000e1e0 value=[1 2 3] // same memory address
s[:0]   |  len=0 cap=3 address=0xc00000e1e0 value=[] // same memory address
s[:3]   |  len=3 cap=3 address=0xc00000e1e0 value=[1 2 3] // same memory address
s[2:]   |  len=1 cap=1 address=0xc00000e1f0 value=[3] // oops, we got array in a new memory address!

Correct me if I’m wrong, but I guess we when call methods from other packages that slices our array, we must know implementation details since those methods, if they use low limit that could mutate the array and generate a new address with a different length and capacity.

IMO the internals of slicing are kind of tricky to reason about for someone getting started to learn Go.

This is not correct. Reslicing a slice will never result in a new allocation. I tried to draw this with “ASCII art” in my previous post, but I guess it’s unclear.

A slice consists of 3 fields:

  • A pointer to the first element in the slice
  • The length of the slice
  • The capacity of the slice

If I have an array of memory: var arr [10]byte
I can then create a slice from it: var sl = arr[:]
After that, the following is true:

&arr[0] == &sl[0]
len(arr) == len(sl)
len(arr) == cap(sl)

As you’ve noticed, when you slice from the end, it’s the length that shrinks. The pointer to the first element and the capacity remain unchanged.

When you slice from the beginning of the slice, though, what happens instead is the pointer to the first element is changed to be a pointer to the nth element that you’re slicing from (i.e. sl = arr[2:] means to set the pointer to point to &arr[2]). Because the head of the slice has moved forward 2, in this case, the length and capacity have to decrease by 2.

If you write to sl[0], the result will appear in arr[2].

Here’s a runable example: Go Playground - The Go Programming Language

1 Like

I will have a look at this. So the underlying array is still in memory and we are just moving the pointer forward?

I thought we got a new array in memory because the memory address got different in output:

The original address was
address=0xc00000e1e0

after slicing with low limit s[2:], we got a different address:
address=0xc00000e1f0

Does the address printed by %p stand for the slice memory address and not the underlying array memory address?

Does this different address we got after slicing represent a new allocation of the slice in memory?

When you slice from the front of the original array (or slice), the resulting slice just has its pointer incremented to the pointer to the element that you sliced to.

In your example, the difference between the original address, 0xc00000e1e0, and the starting address of s[2:]: 0xc00000e1f0 is 16 bytes.

The first element (index 0) in the original slice is 0xc00000e1e0.
The second element (index 1) in the original slice is 0xc00000e1e8 (0xc00000e1e0 + 8 bytes).
The third element (index 2) in the original slice is 0xc00000e1f0 (0xc00000e1e0 + 16 bytes).

This is why s[2:] gets you 0xc00000e1f0.

Reslicing a slice will never result in a new allocation.

1 Like

Yes, with your example it’s easier to see what’s happening with the address:

slicing with high limit
arr add=0xc0000b8000
sl  add=0xc0000b8000
&arr[0] == &sl[0]: true
len(arr) == cap(sl): true

slicing with low limit
arr add=0xc0000b8000
sl  add=0xc0000b8002 <--- the position has increased
*** &arr[0] == &sl[0]: false ***
*** &arr[2] == &sl[0]: true ***

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.