Пустой слайс в Go: правильная инициализация с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Пустой слайс в Go: как не ошибиться с самого начала

Каждый разработчик на Go рано или поздно сталкивается с вопросом: "как правильно инициализировать пустой слайс?". Вроде бы простая задача, но не всё так однозначно. В официальном FAQ по Go и на Stack Overflow этой теме посвящены сотни комментариев и тысячи голосов. Почему?

Потому что в Go есть два принципиально разных способа создать пустой слайс, и выбор между ними влияет на производительность, читаемость кода и даже на поведение при сериализации. Если вы пишете на Go и хотите писать чистый, эффективный код без сюрпризов — эта статья для вас.

Мы разберём:

  • Чем отличается var s []int от s := make([]int, 0)
  • Когда использовать nil-слайс, а когда — пустой слайс
  • Как это влияет на JSON, память и производительность
  • Что говорят гуру Go и авторы стандартной библиотеки

Два брата-близнеца: nil slice vs empty slice

В Go слайс — это структура из трёх полей: указатель на массив, длина и ёмкость. Когда мы говорим о пустом слайсе, мы имеем в виду слайс с длиной 0. Но вот как мы его создаём — имеет значение.

Способ 1: var-объявление (nil slice)

var s []int  // s == nil, len(s) == 0, cap(s) == 0

Это nil-слайс. Он не указывает ни на какой массив в памяти. При этом вы можете смело вызывать на нём append — Go выделит память автоматически.

Способ 2: make-инициализация (empty slice)

s := make([]int, 0)  // s != nil, len(s) == 0, cap(s) == 0

Это пустой слайс. Он уже указывает на нулевой массив (специальный zerobase в рантайме Go).

На первый взгляд разница незаметна. Но давайте копнём глубже.

Вывод в консоль и JSON — первая ловушка

Попробуйте запустить этот код:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var nilSlice []int
    emptySlice := make([]int, 0)

    fmt.Printf("nilSlice: %v, is nil: %v\n", nilSlice, nilSlice == nil)
    fmt.Printf("emptySlice: %v, is nil: %v\n", emptySlice, emptySlice == nil)

    // JSON-сериализация
    nilJSON, _ := json.Marshal(nilSlice)
    emptyJSON, _ := json.Marshal(emptySlice)

    fmt.Printf("nilSlice JSON: %s\n", nilJSON)
    fmt.Printf("emptySlice JSON: %s\n", emptyJSON)
}

Результат вас удивит:

nilSlice: [], is nil: true
emptySlice: [], is nil: false
nilSlice JSON: null
emptySlice JSON: []

Вот он, главный сюрприз! JSON-сериализация ведёт себя по-разному. Если ваш API ожидает [] в ответе, а вы передаёте nil-слайс — клиент получит null. И это ломает контракты.

Многие REST-клиенты (особенно на JavaScript) жёстко проверяют: если в ответе ожидается массив, а приходит null — может вылететь ошибка или нарушиться логика на фронтенде.

Производительность: есть ли разница?

Короткий ответ: да, но в 99% случаев она незначительна.

Когда вы используете make([]int, 0), Go сразу выделяет память под структуру слайса (24 байта на 64-битной системе) и связывает его с zerobase. При использовании var — выделение происходит только при первом append.

Если вы создаёте слайс, который, скорее всего, останется пустым, nil-слайс экономит одно выделение памяти. Если же вы точно знаете, что будете добавлять элементы — make с указанием ёмкости даст лучшую производительность:

// Если знаете примерный размер — укажите capacity
s := make([]int, 0, 100)  // ёмкость 100, длина 0
for i := 0; i < 100; i++ {
    s = append(s, i)  // без переаллокаций!
}

Но для большинства прикладных задач разница в наносекундах. Гораздо важнее семантика и читаемость кода.

Что говорят авторы Go и стандартная библиотека

В официальном FAQ (golang.org/doc/faq#nil_slices) чётко сказано: "nil-слайс функционально эквивалентен пустому слайсу". Всё, что вы можете делать с пустым слайсом, можно делать и с nil-слайсом — за исключением JSON-сериализации.

Более того, в стандартной библиотеке Go nil-слайсы используются повсеместно. Например, функция strings.Split("", ",") возвращает []string{""} (не nil), а bytes.Split(nil, []byte{','}) — nil-слайс. Это особенность реализации, но она показывает, что nil — это нормально.

Роб Пайк (один из создателей Go) в своём выступлении на GopherCon советовал: "Используйте var-объявление, если не нужно явно указывать ёмкость". Это делает код чище и понятнее.

Практические рекомендации: когда что использовать

Давайте сформулируем чёткие правила, которые помогут вам не ошибиться:

Используйте nil-слайс (var declaration), когда:

  • Вы возвращаете слайс из функции, и он может быть пустым
  • Слайс — это поле структуры, которое может отсутствовать
  • Вы планируете проверять == nil для определения, есть ли данные
  • JSON с null для вас семантически корректен

Используйте пустой слайс (make), когда:

  • Вы сериализуете данные в JSON/BSON и хотите получить [], а не null
  • Вы передаёте слайс в функцию, которая ожидает ненулевой слайс (редко, но бывает)
  • Вы собираетесь добавлять элементы сразу после создания
  • Вам нужно явно указать ёмкость для оптимизации

Живой пример: как это выглядит в реальном проекте

Представьте, что вы пишете REST API на Go с использованием Gin или Echo. Типичный эндпоинт возвращает список пользователей:

func GetUsers(c *gin.Context) {
    users, err := db.QueryUsers()
    if err != nil {
        c.JSON(500, gin.H                

Блоги

Рекомендаций по книгам