JavaRush /Курсы /Go SELF /Nil slice vs empty slice: инициализация и проверки

Nil slice vs empty slice: инициализация и проверки

Go SELF
11 уровень , 3 лекция
Открыта

1. nil-слайс и empty-слайс: в чём разница

Когда вы пишете программу на Go, вы очень быстро начинаете хранить данные «списком»: числа, строки, результаты ввода, накопленные значения в цикле. И почти сразу возникает ситуация: «список пустой». Но пустой как именно? Его вообще нет (не инициализирован), или он существует, но в нём 0 элементов?

На человеческом языке звучит как занудство. На языке программирования это иногда важная разница: она влияет на проверки, на то, что можно сравнивать, и на то, как ваш код читается другими людьми (включая будущего вас, который будет смотреть на этот код через месяц и думать: “кто это написал?”).

Zero value для []T: что означает nil-слайс

В Go почти у каждого типа есть значение по умолчанию (zero value). Для int это 0, для boolfalse, для string — пустая строка "". Для слайса []T zero value — это nil. То есть «слайса нет»: он не указывает ни на какие элементы.

Самая частая форма, где вы это увидите — обычное объявление переменной:

package main

import "fmt"

func main() {
	var nums []int

	fmt.Println(nums == nil)          // true
	fmt.Println(len(nums), cap(nums)) // 0 0
	fmt.Println(nums)                 // []
}

Обратите внимание на маленькую «подставу для новичка»: печать fmt.Println(nums) показывает []. На глаз это выглядит как «пустой список». Но технически это nil-слайс.

Важно запомнить три факта про nil-слайс:

nums == nil        → true
len(nums)          → 0
cap(nums)          → 0

Почему это так укладывается в модель «pointer + len + cap»? Потому что pointer = nil, а длина и ёмкость равны нулю — «смотреть некуда и нечего».

2. Знакомство с make

В Go есть два похожих на вид слова — new и make. На старте проще запомнить одно: make используют для создания готовых к работе контейнеров. В первую очередь это слайсы, мапы и каналы. Сегодня нам важны только слайсы.

Когда вы пишете:

tasks := make([]string, 0)

вы просите Go: “сделай мне слайс строк длины 0”. То есть элементов пока нет, но объект-слайс уже существует и его можно безопасно передавать, возвращать из функций и наращивать через append.

У make для слайсов есть две формы — и обе читаются очень буквально:

make([]T, n)      // длина n: элементы уже “существуют” и заполнены zero value
make([]T, n, cap) // длина n и запас ёмкости cap (cap >= n)

Например, это не “пустой список на 3 места”, а список из трёх элементов, просто пока нулевых:

package main

import "fmt"

func main() {
	a := make([]int, 3)
	fmt.Println(a, len(a), cap(a)) // [0 0 0] 3 3
}

А вот это — именно “пока пусто, но есть запас под рост”:

package main

import "fmt"

func main() {
	b := make([]int, 0, 3)
	fmt.Println(b, len(b), cap(b)) // [] 0 3
}

И последнее: make нужен не потому, что без него “нельзя”, а потому что он позволяет явно выбрать форму слайса: создать сразу длину n для заполнения по индексам или создать пустой с запасом, если вы будете добавлять элементы постепенно.

3. Пустой слайс: что это и как его получить

Пустой слайс — это слайс, у которого len == 0, но при этом он не равен nil. То есть «слайс есть», просто в нём пока ноль элементов.

Есть два самых популярных способа получить пустой слайс:

Литерал []T{}

Этот вариант удобно использовать, когда вы хотите прямо в коде явно показать: «это пустая коллекция, но коллекция».

package main

import "fmt"

func main() {
	tasks := []string{}

	fmt.Println(tasks == nil)           // false
	fmt.Println(len(tasks), cap(tasks)) // 0 0
	fmt.Println(tasks)                  // []
}

make([]T, 0)

Это почти то же самое по смыслу, но выглядит как «мы создаём слайс программно».

package main

import "fmt"

func main() {
	tasks := make([]string, 0)

	fmt.Println(tasks == nil)           // false
	fmt.Println(len(tasks), cap(tasks)) // 0 0
}

Технически []string{} и make([]string, 0) обычно приводят к одинаковой картине: длина 0, ёмкость 0, не nil. Но в голове вы можете держать простую ассоциацию: литерал — «пустое значение в коде», make — «создали контейнер».

Шпаргалка: nil vs empty

Когда различия начинают путаться, лучше на минуту остановиться и свериться с простой таблицей. Она экономит много времени и нервов.

Состояние слайса Как создать s == nil len(s) cap(s) Печать fmt.Println(s)
nil-слайс
var s []int
true
0
0
[]
empty-слайс
s := []int{}
false
0
0
[]
empty с запасом ёмкости
s := make([]int, 0, 10)
false
0
10
[]

Отдельно обратите внимание на третью строку: len == 0, но cap может быть большим. Это нормальная ситуация: «элементов пока нет, но место под рост приготовлено».

4. Проверки и инициализация: len(s) == 0 и s == nil

Какие проверки правильные: len(s) == 0 или s == nil

Когда вы проверяете слайс, важно задавать себе честный вопрос: что именно я хочу узнать? И вот тут начинаются самые частые ошибки.

Если вы хотите узнать «есть ли элементы», то правильный вопрос к коду звучит так: длина равна нулю или нет? Значит, проверка должна быть через len.

if len(tasks) == 0 {
	fmt.Println("Задач пока нет") // Задач пока нет
}

Если вы проверяете tasks == nil, вы спрашиваете другое: «слайс не инициализирован / отсутствует как значение?». Это может быть важно, но это уже про смысл данных, а не про наличие элементов.

Чтобы почувствовать разницу, давайте посмотрим пример, где обе проверки дают разный результат:

package main

import "fmt"

func main() {
	var a []int  // nil
	b := []int{} // empty

	fmt.Println(len(a) == 0, a == nil) // true true
	fmt.Println(len(b) == 0, b == nil) // true false
}

И вот теперь правило, которое обычно делает код новичка спокойнее:

  • Если вам нужно «проверить пустоту списка», проверяйте len(s) == 0.
  • Если вам нужно «проверить, инициализирован ли слайс», проверяйте s == nil.

Когда s == nil действительно имеет смысл

Иногда различие nil и empty важно, просто не так часто в самых первых программах.

Представьте, что у вас есть функция, которая опционально собирает данные. Если данных нет вообще (например, пользователь не включал эту опцию), вы хотите вернуть именно nil, чтобы показать «значение отсутствует». А если пользователь включал опцию, но результатов ноль — тогда вернуть empty, чтобы показать «значение есть, но оно пустое».

Давайте смоделируем это без сложных тем:

package main

import "fmt"

func buildHints(enabled bool) []string {
	if !enabled {
		return nil // подсказки отключены
	}
	return []string{} // включены, но пока пусто
}

func main() {
	a := buildHints(false)
	b := buildHints(true)

	fmt.Println(a == nil, len(a)) // true 0
	fmt.Println(b == nil, len(b)) // false 0
}

Здесь len одинаковый (0), но смысл разный. И это как раз ситуация, где == nil полезен: не для «пустоты», а для «присутствия значения».

Аккуратная инициализация: что выбрать в реальном коде

Когда вы начинаете писать функции и передавать слайсы между ними, у вас появляется стиль. И лучше его выработать сейчас, пока программа маленькая.

Если вы собираете список через append (подробнее о ней в следующей лекции) и у вас нет требований про предвыделение памяти, то var s []T — абсолютно нормальный старт. Он короткий, читается и не заставляет вас думать про make раньше времени.

Если вы хотите явно показать «это точно коллекция, даже если она пустая», то s := []T{} тоже хороший вариант. Он часто встречается, когда вы возвращаете значение из функции и хотите гарантировать, что вызывающая сторона получит не-nil.

Если вы заранее знаете, что добавите примерно N элементов, и не хотите, чтобы append несколько раз расширял внутренний буфер, тогда появляется make([]T, 0, n). Но это уже больше про эффективность и предсказуемость роста, и в рамках сегодняшней лекции нам важнее именно смысл nil vs empty, а не оптимизации.

5. Что безопасно делать с nil-слайсом

Когда люди впервые видят nil-слайс, часто возникает тревога: «Если там nil, значит сейчас всё упадёт». Хорошая новость: Go довольно дружелюбен к nil-слайсам. Их можно безопасно использовать во многих местах.

len, cap и range — безопасны

Поскольку len(nilSlice) == 0, цикл просто не выполнится:

package main

import "fmt"

func main() {
	var nums []int // nil

	sum := 0
	for _, v := range nums {
		sum += v
	}

	fmt.Println(sum) // 0
}

Это очень важная практическая вещь: из-за неё nil-слайс часто можно держать как «пустой список по умолчанию», не плодя лишних if’ов.

Индексация s[0] — НЕ безопасна при len(s) == 0

Вот это место, где падают все, включая опытных разработчиков, просто реже. Индексация работает только по диапазону 0..len(s)-1. Поэтому и nil, и empty одинаково опасны для s[0], если длина нулевая.

package main

import "fmt"

func main() {
	var nums []int // nil

	if len(nums) > 0 {
		fmt.Println(nums[0])
	} else {
		fmt.Println("первого элемента нет") // первого элемента нет
	}
}

Почему nil и empty выглядят одинаково при печати

Когда fmt.Println печатает слайс, он печатает «представление элементов». Если элементов ноль, то с точки зрения вывода разницы почти нет: “печатать нечего”.

Но внутри программа различает два состояния:

  • nil-слайс: «нет массива-основания, указатель nil»
  • empty-слайс: «массив-основание может существовать (или быть оптимизирован), но длина 0»

С точки зрения модели слайса (указатель/len/cap), это означает: у nil указатель “пустой”, а у empty — указатель может быть не nil, но длина всё равно 0. Именно поэтому s == nil разделяет эти случаи, а len(s) == 0 — нет.

Если вам хочется «увидеть» разницу в отладке, удобно использовать Printf и печатать и len, и cap, и проверку == nil:

package main

import "fmt"

func main() {
	var a []int
	b := []int{}
	c := make([]int, 0, 3)

	fmt.Printf("a: len=%d cap=%d nil=%v\n", len(a), cap(a), a == nil) // a: len=0 cap=0 nil=true
	fmt.Printf("b: len=%d cap=%d nil=%v\n", len(b), cap(b), b == nil) // b: len=0 cap=0 nil=false
	fmt.Printf("c: len=%d cap=%d nil=%v\n", len(c), cap(c), c == nil) // c: len=0 cap=3 nil=false
}

6. Пример: список задач в учебном приложении

Сейчас у нас идеальный момент, чтобы привязать тему к чему-то «живому», а не к абстрактным nums. Давайте представим, что мы пишем простейшую консольную программу “TaskBox”: она читает задачи и выводит их. До слайсов многие бы пытались хранить задачи в массиве фиксированного размера, но теперь логичнее использовать []string.

Главная цель этого раздела — почувствовать, что nil-слайс не мешает нормальной работе, если мы правильно делаем проверки.

Печать списка задач: правильная проверка на «пусто»

Теперь сделаем маленькую функцию, которая красиво печатает задачи. Важно: она должна одинаково корректно работать и для nil, и для empty.

package main

import "fmt"

func printTasks(tasks []string) {
	if len(tasks) == 0 {
		fmt.Println("Список задач пуст") // Список задач пуст
		return
	}

	for i, t := range tasks {
		fmt.Printf("%d) %s\n", i+1, t)
	}
}

func main() {
	var tasks []string // nil
	printTasks(tasks)

	tasks = append(tasks, "Сделать лекцию 53 понятной")	// добавляем новый текст в конец слайса
	printTasks(tasks)
}

Ключевой момент: мы не спрашиваем tasks == nil. Нам не важно, почему задач нет — потому что список не создавали или потому что создали пустым. Нам важно только, есть ли элементы.

7. Типичные ошибки: nil-слайс и empty-слайс

Ошибка №1: проверять “пустоту” через s == nil.
Такой код работает “иногда” — ровно до момента, когда кто-то создаст s := []T{} или make([]T, 0). Слайс будет пустой, но не nil, и проверка сломает логику. Если вы хотите узнать, есть ли элементы, задавайте этот вопрос напрямую: len(s) == 0.

Ошибка №2: думать, что fmt.Println(s) показывает разницу между nil и empty.
Оба варианта чаще всего печатаются как [], и визуально кажется, что это одно и то же. Если различие важно, печатайте s == nil и len/cap через Printf, иначе вы будете отлаживать “по тени на стене”.

Ошибка №3: индексировать без проверки длины.
Очень соблазнительно написать first := s[0] и считать, что “ну у меня же есть список”. Но nil и empty одинаково падают на s[0], если len(s) == 0. Надёжный шаблон: сначала if len(s) == 0 { ... }, потом уже индексация.

Ошибка №4: усложнять код проверками nil там, где достаточно len.
Новички иногда пишут if s != nil && len(s) > 0 { ... }. Вторая часть (len(s) > 0) уже гарантирует, что в слайсе есть элементы. Дополнительная проверка на nil не делает код безопаснее, зато делает его длиннее и менее читаемым. В Go часто выигрывает самый прямой вопрос к данным: “сколько элементов?”.

1
Задача
Go SELF, 11 уровень, 3 лекция
Недоступна
Три профиля
Три профиля
1
Задача
Go SELF, 11 уровень, 3 лекция
Недоступна
Режим пустоты
Режим пустоты
1
Задача
Go SELF, 11 уровень, 3 лекция
Недоступна
Нормализация nil
Нормализация nil
1
Задача
Go SELF, 11 уровень, 3 лекция
Недоступна
Первый элемент
Первый элемент
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ