1. nil-слайс и empty-слайс: в чём разница
Когда вы пишете программу на Go, вы очень быстро начинаете хранить данные «списком»: числа, строки, результаты ввода, накопленные значения в цикле. И почти сразу возникает ситуация: «список пустой». Но пустой как именно? Его вообще нет (не инициализирован), или он существует, но в нём 0 элементов?
На человеческом языке звучит как занудство. На языке программирования это иногда важная разница: она влияет на проверки, на то, что можно сравнивать, и на то, как ваш код читается другими людьми (включая будущего вас, который будет смотреть на этот код через месяц и думать: “кто это написал?”).
Zero value для []T: что означает nil-слайс
В Go почти у каждого типа есть значение по умолчанию (zero value). Для int это 0, для bool — false, для 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-слайс | |
|
|
|
|
| empty-слайс | |
|
|
|
|
| empty с запасом ёмкости | |
|
|
|
|
Отдельно обратите внимание на третью строку: 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 часто выигрывает самый прямой вопрос к данным: “сколько элементов?”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ