Срезы в Go: Почему append меняет исходный массив?

Почему append меняет исходный массив

Разберем один классический пример, который наглядно демонстрирует важные особенности внутренней работы срезов в языке Go.

Исходный код

package main

import "fmt"

func main() {
    c := []string{"A", "B", "D", "E"}
    b := c[1:2]
    b = append(b, "TT")

    fmt.Println(c) // что выведет эта строка?
    fmt.Println(b) // а эта?
}

Детальный разбор

Шаг 1: Инициализация среза c

Создается срез c с 4 элементами. В памяти выделяется массив, содержащий строки «A», «B», «D», «E». Срез c состоит из:

  • Указателя (pointer) на этот массив
  • Длины (length) = 4
  • Емкости (capacity) = 4

Шаг 2: Создание среза b

b := c[1:2]

Здесь создается новый срез b, который ссылается на тот же самый массив, что и срез c, но начиная с индекса 1 и длиной 1 элемент.

Параметры среза b:

  • Указатель: на элемент с индексом 1 массива (элемент «B»)
  • Длина = 1
  • Емкость = 3 (исходная емкость 4 минус смещение 1)

Шаг 3: Операция append

b = append(b, "TT")

Это самый интересный момент. Давайте разберем, что происходит:

  1. Проверка емкости: Текущая емкость b равна 3, а длина равна 1.
  2. Достаточно ли метса?: Да, для добавления одного элемента достаточно (1 + 1 ≤ 3)
  3. Запись в существующий массив: Поскольку места достаточно, «ТТ» записывается в следующую доступную ячейку того же массива.

Ключевой момент

Оба среза c и b разделяют один и тот же базовый массив! Когда добавляется «ТТ» в b, фактически изменяется элемент с индексом 2 в массиве, которые также видим через срез с.

Результат выполнения

fmt.Println(c) // вывод: [A B TT E]
fmt.Println(b) // вывод: [B TT]

Правило append

  • Если при операции append емкости среза достаточно, новый элемент добавляется в существующий массив.
  • Если емкости недостаточно, создается новый массив с увеличенной емкостью (как правило в 2 раза до определенного количества элементов), и данные копируются в него.

В нашем случае емкости было достаточно, поэтому изменения отразились на исходном массиве.

Практические выводы

1. Будьте осторожны с операциями над под-срезами

original := []int{1, 2, 3, 4, 5}
subset := original[1:4] // использует тот же массив
subset[0] = 99 // меняет original[1]!

2. Как избежать неожиданных изменений

Используйте copy для создания независимых копий:

original := []string{"A", "B", "C", "D"}
independent := make([]string, len(original[1:3]))
copy(independent, original[1:3])

Или используйте полное выражение среза с указанием емкости:

b := c[1:2:2] // capacity = 2 - 1 = 1
b = append(b, "TT") // теперь создаст новый массив

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

Понимание внутреннего устройства срезов — критически важный навык для разработчика на Go. Данный пример наглядно показывает разницу между значением и ссылкой, а также важность понимания capacity при работе со срезами. Такие знания помогут избежать коварных багов в реальных проектах и писать более надежный код на Go.

Поделитесь в комментариях, сталкивались ли вы с подобными «сюрпризами» в продакшн коде?

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: