58

I am a newbie to Go programming. I have read in go programming book that slice consists of three things: a pointer to an array, length and capacity.

I am getting confused between:

  • nil slices (slice has no underlying array to point to, len = 0, cap=0)
  • non-nil slices where only len = 0, cap = 0
  • empty slices.

Can anyone please tell whether nil and empty slices are same things? If they both are different, then please tell what is the difference between those two? How to test whether a slice is empty or not? Also, what value does the pointer holds in non-nil slices, whose length and capacity are zero?

icza
  • 389,944
  • 63
  • 907
  • 827
Surbhi Vyas
  • 719
  • 1
  • 6
  • 10

3 Answers3

91

Observable behavior

nil and empty slices (with 0 capacity) are not the same, but their observable behavior is the same (almost all the time). By this I mean:

  • You can pass them to the builtin len() and cap() functions
  • You can for range over them (will be 0 iterations)
  • You can slice them (by not violating the restrictions outlined at Spec: Slice expressions; so the result will also be an empty slice)
  • Since their length is 0, you can't change their content (appending a value creates a new slice value)

See this simple example (a nil slice and 2 non-nil empty slices):

var s1 []int         // nil slice
s2 := []int{}        // non-nil, empty slice
s3 := make([]int, 0) // non-nil, empty slice

fmt.Println("s1", len(s1), cap(s1), s1 == nil, s1[:], s1[:] == nil)
fmt.Println("s2", len(s2), cap(s2), s2 == nil, s2[:], s2[:] == nil)
fmt.Println("s3", len(s3), cap(s3), s3 == nil, s3[:], s3[:] == nil)

for range s1 {}
for range s2 {}
for range s3 {}

Output (try it on the Go Playground):

s1 0 0 true [] true
s2 0 0 false [] false
s3 0 0 false [] false

(Note that slicing a nil slice results in a nil slice, slicing a non-nil slice results in a non-nil slice.)

Besides an exception, you can only tell the difference by comparing the slice value to the predeclared identifier nil, they behave the same in every other aspect. Do note however that many packages do compare slices to nil and may act differently based on that (e.g. encoding/json and fmt packages).

The only difference is by converting the slice to an array pointer (which was added to the language in Go 1.17). Converting a non-nil slice to an array pointer will result in a non-nil pointer, converting a nil slice to an array pointer will result in a nil pointer.

To tell if a slice is empty, simply compare its length to 0: len(s) == 0. It doesn't matter if it's the nil slice or a non-nil slice, it also doesn't matter if it has a positive capacity; if it has no elements, it's empty.

s := make([]int, 0, 100)
fmt.Println("Empty:", len(s) == 0, ", but capacity:", cap(s))

Prints (try it on the Go Playground):

Empty: true , but capacity: 100

Under the hood

A slice value is represented by a struct defined in reflect.SliceHeader:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

In case of a nil slice, this struct will have its zero value which is all its fields will be their zero value, that is: 0.

Having a non-nil slice with both capacity and length equal to 0, Len and Cap fields will most certainly be 0, but the Data pointer may not be. It will not be, that is what differentiates it from the nil slice. It will point to a zero-sized underlying array.

Note that the Go spec allows for values of different types having 0 size to have the same memory address. Spec: System considerations: Size and alignment guarantees:

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

Let's check this. For this we call the help of the unsafe package, and "obtain" the reflect.SliceHeader struct "view" of our slice values:

var s1 []int
s2 := []int{}
s3 := make([]int, 0)

fmt.Printf("s1 (addr: %p): %+8v\n",
    &s1, *(*reflect.SliceHeader)(unsafe.Pointer(&s1)))
fmt.Printf("s2 (addr: %p): %+8v\n",
    &s2, *(*reflect.SliceHeader)(unsafe.Pointer(&s2)))
fmt.Printf("s3 (addr: %p): %+8v\n",
    &s3, *(*reflect.SliceHeader)(unsafe.Pointer(&s3)))

Output (try it on the Go Playground):

s1 (addr: 0x1040a130): {Data:       0 Len:       0 Cap:       0}
s2 (addr: 0x1040a140): {Data: 1535812 Len:       0 Cap:       0}
s3 (addr: 0x1040a150): {Data: 1535812 Len:       0 Cap:       0}

What do we see?

  • All slices (slice headers) have different memory addresses
  • The nil slice has 0 data pointer
  • s2 and s3 slices do have the same data pointer, sharing / pointing to the same 0-sized memory value
icza
  • 389,944
  • 63
  • 907
  • 827
  • 1
    Can you please explain a bit about what exactly is 0-sized memory value? I mean is it like, for empty slices, there is underlying array having memory allocated but no elements are there OR is it like there is no memory allocated underneath and the data pointer in the slice structure holds any random address? – Surbhi Vyas Jun 02 '17 at 08:12
  • 1
    @SurbhiVyas The memory address doesn't have any meaning in a sense that where it points to contains no data, or rather the pointed area is not to be read because only 0 bytes is reserved for it (that is: nothing). It is a special, reserved address for variables of different types having 0 size. It's an implementation detail, you should not worry about it or make anything of it. – icza Jun 02 '17 at 08:30
  • Thank you for the complete explanation. – Surbhi Vyas Jun 02 '17 at 10:06
  • 3
    the important thing to notice here, go's `append` function will not panic if provided a slice of nil value. Semantically, it treats such an input as an _empty_ slice and append to it as such. This avoids a lot of boiler-plate nil checks in user code. – colm.anseo Feb 22 '19 at 21:59
  • *"they behave the same in every other aspect"* - that's not quite correct. For example, `json.Marshal(s)` returns `"null"` if `s` is `nil`, but `"[]"` if `s` is empty (but not `nil`). I guess there are other cases where `nil` and empty slices behave differently. – jcsahnwaldt Reinstate Monica Dec 04 '22 at 13:52
  • @jcsahnwaldtReinstateMonica You quoted only half of the sentence. The full sentence is: _"You can only tell the difference by comparing the slice value to the predeclared identifier `nil`, they behave the same in every other aspect."_ And that's exactly what the `encoding/json` package does: it compares it to `nil`. – icza Dec 04 '22 at 15:49
  • @icza Yes, I'm being a bit picky, but... if someone told me "you can only tell the difference by comparing the slice value to nil, they behave the same in every other aspect", and then I found out that they behave differently when I pass them to `json.Marshal()`, I'd say "you were wrong, I didn't compare them to nil, and yet they didn't behave the same way". (Why and how `Marshal()` does that is an implementation detail.) The correct wording would be "behave the same in most other aspects" or something like this; "behave the same in every other aspect" is misleading. – jcsahnwaldt Reinstate Monica Dec 06 '22 at 00:17
  • Here's another aspect in which nil and empty slices don't behave the same: `var n []int = nil; e := []int{}; np := (*[0]int)(n); ep := (*[0]int)(e); fmt.Println(np, ep)` I'm pretty sure no slice gets compared to nil when you run this code. :-) – jcsahnwaldt Reinstate Monica Dec 06 '22 at 00:23
  • @jcsahnwaldtReinstateMonica What you have there are not slices but **pointers to arrays**, that's a completely different thing (slices and arrays are not the same). Not to mention zero-sized types are a completely different topic (as they may be placed to the same memory location according to the spec, and `[0]int` has zero size). Also, the `fmt` package does a lot of "magic" under the hood, just like the `encoding/json` package. It's like you write an `f()` function that takes a slice, and inside it you compare the slice to `nil` and do different stuff based on that... – icza Dec 06 '22 at 07:20
  • 1
    @jcsahnwaldtReinstateMonica Nevertheless, I added a note in bold to bring this to attention. – icza Dec 06 '22 at 07:26
  • @icza I know all that. My example happens to deal with pointers to the underlying arrays, but that's not really the point. The example creates two slices (one nil, one empty) and demonstrates that they behave differently, although there's no comparison of a slice to nil anywhere. The point is: nil and empty slices can behave differently in several ways, even if you don't compare them to nil. The added note is good, but it doesn't cover this case (and possibly many other cases where nil and empty slices behave differently, even without any comparison to nil). – jcsahnwaldt Reinstate Monica Dec 06 '22 at 07:40
  • 1
    @jcsahnwaldtReinstateMonica OK, I got your point. Do note however that conversion from slice to array pointer was only added in Go 1.17, before that it was not allowed. Edited the answer. – icza Dec 06 '22 at 08:18
  • @icza Thanks for humoring my pickiness. :-) By the way, getting the array pointer was possible before 1.17, but the syntax was a lot more cumbersome. https://stackoverflow.com/questions/36706843/how-to-get-the-underlying-array-of-a-slice-in-go – jcsahnwaldt Reinstate Monica Dec 06 '22 at 09:30
  • @jcsahnwaldtReinstateMonica Yes, using package `unsafe`, but let's not reason about that solution as package `unsafe` steps around the type safety of Go programs. It's not an everyday tool for a Go programmer (but rather a last resort). – icza Dec 06 '22 at 09:45
2

A slice can be nil in a very literal sense:

var n []int
n == nil // true

That is the only easy case. The notion of "empty" slice is not well defined: A slice s with len(s) == 0 is certainly empty, regardless of its capacity. The most sensible thing to do is: Ignore the underlying implementation, one never needs to know how a slice is represented internally. All it matters is the defined behaviour of a slice.

How to test whether a slice is empty or not?

The most sensible definition of " slice s is empty" is a slice containing no elements which translates to len(s) == 0. This definition holds for nil as well as for non-nil slices.

Can anyone please tell whether nil and empty slices are same things? If they both are different, then please tell what is the difference between those two?

Technically a nil slice and a non-nil slice are different (one being == nil, the other being != nil), but this distinction typically does not matter as you can append to a nil slice and len and cap on nil slices return 0

var n []int
len(n) == cap(n) == 0 // true
n = append(n, 123)
len(n) == 1 // true

Read about zero values in Go for further information. A nil slice is like a nil channel or a nil map: It is uninitialised. You initialise by either makeing them or by a literal. As said above, there is no reason to think about the underlying representation.

Also, what value does the pointer holds in non-nil slices, whose length and capacity are zero?

This is an implementation detail which may vary from compiler to compiler or even from version to version. Nobody needs to know this to write correct and portable Go programs.

Volker
  • 40,468
  • 7
  • 81
  • 87
  • 4
    To the last question, per [the spec](https://golang.org/ref/spec#Slice_types), "A slice created with make always allocates a new, hidden array to which the returned slice value refers." – Adrian Jun 01 '17 at 13:36
2
var s1 []int         // nil slice
s2 := []int{}        // non-nil, empty slice
s3 := make([]int, 0) // non-nil, empty slice

warning, if dealing with JSON, a nil slice will encode to null instead of [], which can break some (javascript) clients that try to iterate (without a null check) over null which isn't iterable.

Rusty Rob
  • 16,489
  • 8
  • 100
  • 116