Avatar ·

Как правильно инициализировать пустой срез в Go: var s []int vs s := make([]int, 0)?

Всем привет! Я начал изучать Go недавно и столкнулся с проблемой инициализации пустых срезов. Вроде бы есть два популярных способа: 1. `var s []int` — объявление без инициализации (nil-срез) 2. `s := make([]int, 0)` — инициализация пустым срезом через make Я читал, что оба варианта можно использовать, и в некоторых случаях они ведут себя одинаково. Например, если я делаю `fmt.Println(s)`, то в обоих случаях выводится `[]`. Но я заметил странное поведение при сериализации в JSON. Вот пример: ```go package main import ( "encoding/json" "fmt" ) type MyStruct struct { Items []string } func main() { s1 := MyStruct{} s2 := MyStruct{Items: []string{}} json1, _ := json.Marshal(s1) json2, _ := json.Marshal(s2) fmt.Println(string(json1)) // {"Items":null} fmt.Println(string(json2)) // {"Items":[]} } ``` Почему в одном случае выводится `null`, а в другом — пустой массив? Это сбивает с толку, особенно когда я работаю с API и ожидаю, что клиент получит `[]`, а не `null`. Какой способ считается правильным идиоматическим в Go? Есть ли разница в производительности? И когда какой вариант использовать? Заранее спасибо за помощь! <<>>

Ключевое различие: nil-срез vs пустой срез

В Go существует важное различие между nil-срезом (объявленным через var s []T) и пустым срезом (созданным через make([]T, 0) или литералом []T{}). Оба ведут себя одинаково в большинстве операций, но есть нюансы.

Основные отличия

  • nil-срез: var s []int — указатель на nil, длина 0, ёмкость 0. Не имеет выделенной памяти.
  • Пустой срез: s := make([]int, 0) — указатель на пустую область памяти, длина 0, ёмкость 0. Память выделена.

Когда они ведут себя одинаково

В большинстве операций разницы нет:

var s1 []int          // nil-срез
s2 := make([]int, 0)  // пустой срез

// Оба работают с append
s1 = append(s1, 1)    // [1]
s2 = append(s2, 1)    // [1]

// Оба дают длину 0
fmt.Println(len(s1))  // 0
fmt.Println(len(s2))  // 0

// for range не выполняется для обоих
for _, v := range s1 {
    fmt.Println(v)    // не выполнится
}

Когда различие критично

1. Сериализация JSON — самый частый случай, как вы заметили:

type Data struct {
    Items []string
}

d1 := Data{}            // Items = nil
d2 := Data{Items: []string{}} // Items = пустой срез

json1, _ := json.Marshal(d1) // {"Items":null}
json2, _ := json.Marshal(d2) // {"Items":[]}

2. Сравнение с nil:

var s1 []int
s2 := make([]int, 0)

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

3. Передача в функции, проверяющие на nil (например, некоторые библиотеки).

Рекомендации от сообщества Go

  • Используйте var s []T, когда срез может остаться nil (например, поле структуры, которое может быть не задано). Это идиоматично и экономит память.
  • Используйте s := []T{} или make([]T, 0), когда вам нужен гарантированно пустой срез (например, для JSON-сериализации, чтобы получить [] вместо null).
  • Для локальных переменных, которые вы будете наполнять через append, смело используйте var s []T — Go оптимизирует это.

Производительность

Разница минимальна:

  • var s []T — не выделяет память (просто nil-указатель).
  • make([]T, 0) — выделяет небольшой блок памяти (обычно 0 байт, но с заголовком).
  • []T{} — аналогично make, но синтаксически чище.

На практике это не влияет на производительность, но если вы создаёте миллионы срезов, nil-срез будет чуть эффективнее.

Практический совет

Если вы пишете API и хотите, чтобы клиент всегда получал [] вместо null, инициализируйте срез явно:

type Response struct {
    Items []string `json:"items"`
}

func NewResponse() Response {
    return Response{
        Items: []string{}, // всегда пустой массив в JSON
    }
}

Или используйте кастомный тип с кастомной JSON-сериализацией, если нужно более сложное поведение.

<<>>

Альтернативный взгляд: всегда инициализируйте явно

Некоторые разработчики (включая меня) предпочитают всегда инициализировать срезы явно, чтобы избежать путаницы с nil. Вот мои аргументы:

  • Предсказуемость: s := []int{} всегда даёт одинаковое поведение, независимо от контекста.
  • JSON-сериализация: вы никогда не получите null там, где ожидаете [].
  • Безопасность: некоторые функции из стандартной библиотеки (например, reflect.DeepEqual) различают nil и пустой срез.

Пример, где nil-срез может привести к ошибке:

var s []int
s[0] = 1 // panic: runtime error: index out of range [0] with length 0

s2 := []int{}
s2[0] = 1 // тоже panic, но это ожидаемо для пустого среза

Хотя оба варианта паникуют, nil-срез может скрывать баги, если вы случайно забыли его инициализировать. Явная инициализация делает намерения кода яснее.

Мой совет: если сомневаетесь — используйте []T{}. Это безопасный выбор, который не даст сюрпризов. Исключение — если вы работаете с кодом, который явно проверяет на nil (например, if s == nil), тогда используйте var s []T.