Slice - один из наиболее распространенных типов данных, используемых в Golang. Это позволяет нам содержать непрерывные данные любого определенного типа, которые мы можем добавлять или перебирать.

var foo []string
foo = append(foo, "a")
foo = append(foo, "b")
foo = append(foo, "c")
for i, v := range {
  fmt.Println(i, v)
}
// Output
0 a
1 b
2 c

Также стоит отметить, что инициализированный Slice имеет скрытый массив, что означает, что он разделяет хранилище с массивом. Помимо length, Slice также имеет capacity, который является длиной базового массива. Добавление нового значения за пределы емкости приведет к выделению нового массива, который соответствует элементам Slice.

Нулевое значение

В Golang, когда переменная объявляется без значения инициализации, ее значение будет установлено равным нулевому значению типа. Нулевое значение Slice равно nil, поэтому в нашем примере выше, когда мы объявляем var foo []string, значение foo на самом деле nil не является пустым фрагментом строки []. Пустой срез может быть сгенерирован с помощью коротких объявлений переменных, например. foo := []string{} или make функция.

Почему это важно?

var nilSlice []string
emptySlice := make([]string, 5)
fmt.Println(nilSlice) // Output: []
fmt.Println(len(nilSlice), cap(nilSlice)) // Output: 0 0
fmt.Println(nilSlice == nil) // Output: true
fmt.Println(emptySlice) // Output: []
fmt.Println(len(emptySlice), cap(emptySlice)) // Output: 0 0
fmt.Println(emptySlice == nil) // Output: false

Приведенный выше пример демонстрирует, как Nil Slice может обмануть наш глаз. Результат fmt.Println -ing нулевого фрагмента будет [] точно таким же, как у нас для пустого фрагмента. Он также имеет ту же длину и емкость, что и пустой слайс.

В большинстве случаев нам не нужно относиться к этим двоим по-разному. Мы все еще можем добавлять и повторять нулевой фрагмент, даже если его начальное значение равно нулю. Однако есть и другие случаи, в которых могут возникнуть проблемы.

type Res struct {
  Data []string
}
var nilSlice []string
emptySlice := make([]string, 5)
res, _ := json.Marshal(Res{Data: nilSlice})
res2, _ := json.Marshal(Res{Data: emptySlice})
fmt.Println(string(res)) // Output: {"Data":null}
fmt.Println(string(res2)) // Output: {"Data":[]}

Golang encoding/json кодирует Nil Slice в null, что может быть неприемлемым, если наш контракт API определяет Data как ненулевой массив строк.

var nilSlice []string
emptySlice := make([]string, 5)
fmt.Println(reflect.DeepEqual(nilSlice, emptySlice)) // Output: false
fmt.Printf("Got: %+v, Want: %+v\n", nilSlice, emptySlice) //Output: Got: [], Want: []

Другая проблема заключается в том, что мы сравниваем эти два, используя reflect.DeepEqual, который обычно используется под капотом в библиотеке утверждений. Сравнение будет ложным, что приведет к сбою утверждения, но сообщение с утверждением дает нам то же [], которое не дает нам спать.