Как правильно инициализировать пустой срез в Go: var s []int vs s := make([]int, 0)?
Ключевое различие: 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.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги