← Назад к вопросам

Что будет со слайсом, если взять его срез и сделать append?

2.0 Middle🔥 221 комментариев
#Основы Go#Производительность и оптимизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что будет со слайсом при append к его срезу

Это опасный трюк в Go, связанный с тем, что срезы (slices) — это представления (views) на массив, а не копии. Понимание этого поведения критично для избежания сложных багов.

Основной механизм

Срез — это структура из трёх компонентов:

slice := []int{1, 2, 3, 4, 5}
        
┌─────────────────────────────────┐
│ Underlying Array                │
│ [1, 2, 3, 4, 5, ?, ?, ?...]    │
└─────────────────────────────────┘
  ▲
  pointer (указатель на первый элемент)
  length: 5 (количество элементов)
  capacity: 8 (всего памяти в массиве)

Сценарий 1: Append в срез с оставшейся ёмкостью

func main() {
    original := []int{1, 2, 3, 4, 5}  // capacity: 10 (обычно больше length)
    
    // Берём срез с индекса 2
    subslice := original[2:4]  // [3, 4]
    // subslice указывает на позицию 2 в исходном массиве
    // length: 2, capacity: 8 (сколько осталось до конца underlying array)
    
    fmt.Println("Before append:")
    fmt.Println("original:", original)   // [1 2 3 4 5]
    fmt.Println("subslice:", subslice)   // [3 4]
    
    // Добавляем элемент в subslice
    subslice = append(subslice, 99)
    
    fmt.Println("\nAfter append:")
    fmt.Println("original:", original)   // [1 2 3 99 5] ⚠️ ИЗМЕНИЛОСЬ!
    fmt.Println("subslice:", subslice)   // [3 4 99]
    
    // Исходный срез тоже изменился, потому что он указывает на тот же underlying array!
}

Почему это происходит:

  • subslice указывает на элемент с индексом 2 исходного массива
  • Append добавляет элемент в позицию 4 (следующую после конца subslice)
  • Но это же положение (индекс 4) в исходном массиве!
  • Поэтому original[4] изменяется с 5 на 99

Сценарий 2: Append вызывает переаллокацию

func main() {
    // Создаём массив с точной ёмкостью
    original := make([]int, 5, 5)  // length=5, capacity=5
    copy(original, []int{1, 2, 3, 4, 5})
    
    subslice := original[2:4]  // [3, 4]
    // capacity subslice = 3 (5 - 2, осталось до конца)
    
    fmt.Println("Before append:")
    fmt.Println("original:", original)   // [1 2 3 4 5]
    fmt.Println("subslice:", subslice)   // [3 4]
    fmt.Println("capacity:", cap(subslice))  // 3
    
    // Добавляем МНОГО элементов, вызывающих переаллокацию
    subslice = append(subslice, 10, 11, 12, 13)
    
    fmt.Println("\nAfter append:")
    fmt.Println("original:", original)   // [1 2 3 4 5] - не изменилось!
    fmt.Println("subslice:", subslice)   // [3 4 10 11 12 13]
    
    // На этот раз subslice указывает на НОВЫЙ underlying array!
    // Потому что старого места не хватило
}

Почему так:

  • subslice имеет capacity 3
  • Append пытается добавить 4 элемента
  • Go выделяет новый больший массив и копирует данные
  • subslice теперь указывает на новый массив
  • original остаётся неизменным

Сценарий 3: Опасный баг - изменение через срезы

func main() {
    original := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // Два среза от одного массива
    slice1 := original[1:4]    // [2, 3, 4]
    slice2 := original[3:7]    // [4, 5, 6, 7]
    
    fmt.Println("Before:")
    fmt.Println("original:", original)
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)
    
    // Modify через slice1
    slice1[2] = 999  // Изменяем элемент с индексом 3 в original
    
    fmt.Println("\nAfter slice1[2] = 999:")
    fmt.Println("original:", original)  // [1 2 3 999 5 6 7 8 9 10]
    fmt.Println("slice1:", slice1)      // [2 3 999]
    fmt.Println("slice2:", slice2)      // [999 5 6 7] ⚠️ Тоже изменился!
}

Как избежать этих проблем

1. Копируй данные, если нужно изменять

func safe() {
    original := []int{1, 2, 3, 4, 5}
    
    // Делаем копию
    subslice := make([]int, len(original[2:4]))
    copy(subslice, original[2:4])
    
    // Теперь append не влияет на original
    subslice = append(subslice, 99)
    
    fmt.Println("original:", original)  // [1 2 3 4 5] - не изменилось
    fmt.Println("subslice:", subslice)  // [3 4 99]
}

2. Используй full slice expression для управления ёмкостью

func controlCapacity() {
    original := []int{1, 2, 3, 4, 5, 6, 7, 8}
    
    // Берём срез БЕЗ резервной ёмкости
    subslice := original[2:4:4]  // [3, 4], capacity=0 (4-4)
    
    // Теперь append вызовет переаллокацию сразу
    subslice = append(subslice, 99)
    
    fmt.Println("original:", original)  // [1 2 3 4 5 6 7 8] - не изменилось
    fmt.Println("subslice:", subslice)  // [3 4 99]
}

3. Будь осторожен при возврате срезов из функций

// ❌ Опасно
func getBadSlice() []int {
    arr := []int{1, 2, 3, 4, 5}
    return arr[1:4]  // Возвращаем срез локального массива!
}

// ✅ Лучше - копируй
func getGoodSlice() []int {
    arr := []int{1, 2, 3, 4, 5}
    result := make([]int, 3)
    copy(result, arr[1:4])
    return result
}

Практический пример: фильтрация с ошибкой

// ❌ Неправильно
func filterBad(data []int, predicate func(int) bool) []int {
    result := make([]int, 0, len(data))
    for _, v := range data {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result  // Может содержать нежелательные элементы
    // если capacity был > len(data)
}

// ✅ Правильно
func filterGood(data []int, predicate func(int) bool) []int {
    var result []int
    for _, v := range data {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result  // Чистый срез без мусора в capacity
}

Ключевые выводы

  1. Срезы — это представления (views), а не копии данных
  2. Append в срез может изменить исходный массив, если есть свободная ёмкость
  3. Используй copy() если нужны независимые данные
  4. Full slice expression s[i:j:k] контролирует ёмкость
  5. Будь осторожен с возвратом срезов из функций

Это одна из самых хитрых особенностей Go, и понимание её избегает множество сложных багов в production коде.