Avatar ·

Как очистить слайс (slice) в Go? Все способы и подводные камни

Привет, коллеги! Я только начал изучать Go и столкнулся с проблемой, которая, казалось бы, должна быть тривиальной, но я никак не могу найти однозначного решения. У меня есть слайс, который я активно использую в цикле для накопления данных. После каждой итерации мне нужно полностью очистить этот слайс, чтобы начать заполнять его заново. Вот упрощённый пример того, что я делаю: ```go package main import "fmt" func main() { data := make([]int, 0, 10) // создаём слайс с начальной ёмкостью 10 for i := 0; i < 3; i++ { // Наполняем слайс данными for j := 0; j < 5; j++ { data = append(data, j) } fmt.Println("Iteration", i, "data:", data) // А вот тут мне нужно очистить data перед следующей итерацией // data = ??? что сюда писать? } } ``` Я перепробовал несколько вариантов: 1. `data = nil` — это обнуляет слайс, но не освобождает выделенную память? Или освобождает? 2. `data = data[:0]` — это вроде бы сбрасывает длину, но ёмкость остаётся. Будет ли это работать правильно? 3. `data = []int{}` — создаёт новый пустой слайс, но теряется ли при этом исходная ёмкость? Я читал документацию, но всё равно путаюсь. Подскажите, пожалуйста, какой способ считается правильным и идиоматичным в Go? Есть ли разница в производительности между этими подходами? И что происходит с памятью в каждом случае? Заранее спасибо за разъяснения! <<>>

Основной способ: обнуление длины через срез data = data[:0]

В Go наиболее идиоматичный и эффективный способ очистить слайс — это присвоить ему срез от самого себя с нулевой длиной:

data = data[:0]

Этот подход не выделяет новую память и не освобождает существующую. Он просто устанавливает длину слайса в 0, сохраняя при этом его ёмкость (capacity). Все элементы, которые были в слайсе, остаются в памяти, но становятся недоступными через этот слайс. При последующем использовании append они будут перезаписаны новыми данными.

Почему это работает?

В Go слайс — это структура из трёх полей: указатель на базовый массив, длина (len) и ёмкость (cap). Операция data[:0] создаёт новый слайс, который указывает на тот же самый базовый массив, но с длиной 0. При этом ёмкость остаётся прежней, поэтому последующие вызовы append будут использовать уже выделенную память без дополнительных аллокаций.

Когда использовать этот способ?

  • Когда вы планируете повторно использовать ту же самую память для новых данных
  • В циклах, где слайс наполняется и очищается многократно
  • Когда важна производительность и минимизация аллокаций

Пример с вашим кодом

package main

import "fmt"

func main() {
    data := make([]int, 0, 10)

    for i := 0; i < 3; i++ {
        for j := 0; j < 5; j++ {
            data = append(data, j)
        }
        fmt.Println("Iteration", i, "data:", data, "len:", len(data), "cap:", cap(data))

        // Очищаем слайс, сохраняя ёмкость
        data = data[:0]
    }
    fmt.Println("После цикла len:", len(data), "cap:", cap(data)) // len: 0, cap: 10
}

Другие способы и их особенности

1. data = nil

  • Устанавливает слайс в nil (длина и ёмкость становятся 0)
  • Базовый массив остаётся в памяти до тех пор, пока сборщик мусора не решит его освободить
  • При следующем append будет выделен новый массив (новая аллокация)
  • Используйте, если нужно полностью "сбросить" слайс и отказаться от старой памяти

2. data = []int{}

  • Создаёт новый пустой слайс с ёмкостью 0
  • Старый базовый массив остаётся в памяти, но ссылка на него теряется
  • При первом же append будет выделен новый массив
  • Менее эффективно, чем data = data[:0]

3. data = make([]int, 0, cap(data))

  • Создаёт новый слайс с той же ёмкостью
  • Старый массив будет собран сборщиком мусора
  • Полезно, если нужно гарантированно избавиться от старых данных (например, из соображений безопасности)

Что насчёт GC и утечек памяти?

Если вы используете data = data[:0], старые элементы остаются в памяти, но на них больше нет ссылок через слайс. Если элементы — это указатели на большие структуры, сборщик мусора не сможет их освободить, пока вы не перезапишете эти позиции новыми данными. В таких случаях для очистки указателей используйте:

for i := range data {
    data[i] = nil // или zero value
}
data = data[:0]

Это гарантирует, что сборщик мусора сможет освободить память, на которую указывали элементы.

<<>>

Альтернативный подход: полное пересоздание слайса

Если ваша задача — не просто очистить слайс, а гарантированно избавиться от всех старых данных (например, для безопасности или при работе с большими объёмами данных, которые нужно освободить немедленно), используйте полное пересоздание:

data = make([]int, 0, 10) // или data = nil; data = make([]int, 0, 10)

Этот подход имеет два преимущества:

  • Безопасность: старые данные гарантированно становятся недоступными и могут быть собраны GC
  • Предсказуемость: вы точно знаете, что слайс "чистый" и не содержит мусора

Недостаток — небольшие накладные расходы на аллокацию нового массива. Однако в большинстве приложений это не является проблемой. Если ваш код не критичен к производительности (не сотни тысяч операций в секунду), смело используйте этот способ — он самый понятный и безопасный.

Также стоит помнить, что если вы передаёте слайс в функцию по ссылке (через указатель), то внутри функции нужно очищать именно исходный слайс, а не локальную копию:

func clearSlice(s *[]int) {
    *s = (*s)[:0] // или *s = make([]int, 0, cap(*s))
}