Пустой слайс в 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