Avatar ·

Правильный способ инициализации пустого среза в Go: var vs make

Всем привет! Я только начал изучать Go и столкнулся с путаницей при инициализации пустых срезов. В документации и примерах я вижу два основных подхода, но не понимаю, какой из них правильный и в каких ситуациях. Первый способ — через объявление переменной: ```go var s []string ``` Второй — через функцию make: ```go s := make([]string, 0) ``` Я прочитал, что оба создают пустой срез, но есть ли между ними разница? Когда я использую `var s []string`, я могу проверить `s == nil` и получить `true`. А при использовании `make([]string, 0)` — `false`. Значит ли это, что один способ лучше другого? Меня особенно волнует ситуация, когда я создаю срез, который потом будет заполняться данными, например, при парсинге JSON или чтении строк из файла. Я хочу избежать лишних аллокаций памяти и проблем с nil-срезами при сериализации. В некоторых языках, которые я знаю, пустой массив и nil — это разные вещи, и это часто приводило к багам. Подскажите, пожалуйста, какой способ инициализации считается идиоматичным в Go? Когда использовать `var`, а когда `make`? И есть ли третий вариант — литерал `[]string{}`? Заранее спасибо! <<>>

Главное отличие: nil-срез против пустого среза

В Go действительно есть разница между var s []string (nil-срез) и s := make([]string, 0) (пустой срез). Оба имеют длину 0, но ведут себя по-разному при сравнении с nil и сериализации в JSON.

var s1 []string          // nil-срез, len=0, cap=0
s2 := make([]string, 0)  // пустой срез, len=0, cap=0
s3 := []string{}         // пустой срез, len=0, cap=0

fmt.Println(s1 == nil)   // true
fmt.Println(s2 == nil)   // false
fmt.Println(s3 == nil)   // false

Когда использовать var (nil-срез)

Идиоматичный способ в Go — использовать var s []Type, если вы не знаете конечный размер среза и планируете добавлять элементы через append. Это считается правильным подходом, потому что:

  • Не создаёт лишних аллокаций — nil-срез не указывает на underlying array
  • append отлично работает с nil-срезом и автоматически выделит память при первом добавлении
  • Многие функции стандартной библиотеки (например, json.Unmarshal) корректно обрабатывают nil-срезы
var result []string
for _, item := range data {
    result = append(result, process(item))
}
// result останется nil, если data пуст — это нормально

Когда использовать make (пустой срез)

Используйте make([]Type, 0) или литерал []Type{} в следующих случаях:

  • Сериализация JSON: если вы хотите, чтобы пустой срез сериализовался как [], а не null
  • API/протоколы: когда внешняя система ожидает массив, а не null
  • Избежание nil-проверок: если код дальше предполагает, что срез не nil
type Response struct {
    Items []string `json:"items"`
}

// Вариант 1: вернёт {"items": null}
r1 := Response{Items: nil}
json.Marshal(r1) // {"items": null}

// Вариант 2: вернёт {"items": []}
r2 := Response{Items: make([]string, 0)}
json.Marshal(r2) // {"items": []}

Производительность и память

Важно понимать, что make([]string, 0) и []string{} — это одно и то же. Оба варианта выделяют пустой underlying array (обычно 0 байт, но указатель не nil). var s []string вообще не выделяет память. Однако на практике разница в производительности минимальна, если вы сразу начинаете добавлять элементы — первый append всё равно выделит новый массив.

Практические рекомендации

  1. Для накопления данных: используйте var s []Type — это идиоматично и эффективно
  2. Для JSON/API: используйте make([]Type, 0) если важно получить [] вместо null
  3. Для возврата из функций: возвращайте nil-срез, если результата нет — это стандартная практика в Go
  4. Избегайте лишних проверок: len(s) работает одинаково и с nil, и с пустым срезом — возвращает 0
// Правильный паттерн для сбора данных
func getItems() []string {
    var items []string  // nil, если ничего не добавится
    for _, v := range source {
        if condition(v) {
            items = append(items, v)
        }
    }
    return items  // может быть nil — это нормально
}

// Правильный паттерн для JSON
func getItemsJSON() []string {
    items := make([]string, 0)  // гарантирует [] в JSON
    // ... заполнение
    return items
}

В итоге, оба способа корректны. Выбор зависит от контекста: для внутреннего использования предпочтительнее var, для внешних интерфейсов (JSON, gRPC) — make.

<<>>

Дополнение: третий вариант и особенности append

Хочу добавить, что существует и третий способ — []string{}, который полностью эквивалентен make([]string, 0). Разницы между ними нет, это просто вопрос стиля.

a := []string{}  // то же самое, что make([]string, 0)
b := make([]string, 0)  // идентично

Также важно помнить, что append работает одинаково со всеми вариантами:

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

// Все три работают одинаково
s1 = append(s1, "a")  // len=1, cap=1
s2 = append(s2, "a")  // len=1, cap=1
s3 = append(s3, "a")  // len=1, cap=1

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

Мой совет начинающим: пока не столкнётесь с проблемами JSON, используйте var s []Type. Это самый простой и идиоматичный способ. Когда понадобится контролировать сериализацию — переключитесь на make.