Правильный способ инициализации пустого среза в Go: var vs make
Главное отличие: 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 всё равно выделит новый массив.
Практические рекомендации
- Для накопления данных: используйте
var s []Type— это идиоматично и эффективно - Для JSON/API: используйте
make([]Type, 0)если важно получить[]вместоnull - Для возврата из функций: возвращайте nil-срез, если результата нет — это стандартная практика в Go
- Избегайте лишних проверок:
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.
Ответов пока нет.
Войдите чтобы оставить ответ