Какие существуют риски при мутации слайса в Go? Как `append` влияет на базовый массив и другие слайсы, ссылающиеся на него?

Ответ

Мутировать слайс в целом безопасно, но требует понимания его внутренней структуры, чтобы избежать неожиданного поведения. Слайс — это заголовок, содержащий три поля:

  1. Указатель на базовый массив (underlying array).
  2. Длина (len) — количество элементов в слайсе.
  3. Ёмкость (cap) — количество элементов от начала слайса до конца базового массива.

Риски и поведение при мутации:

1. Изменение элементов существующего слайса

Если вы меняете элемент (slice[i] = newValue), вы меняете значение в базовом массиве. Любой другой слайс, который ссылается на этот же массив, увидит эти изменения.

original := []int{1, 2, 3, 4}
sliceA := original[:2] // [1, 2]
sliceB := original[1:3] // [2, 3]

sliceA[1] = 99 // Меняем второй элемент

fmt.Println(original) // Вывод: [1, 99, 3, 4]
fmt.Println(sliceB)   // Вывод: [99, 3] // sliceB тоже изменился!

2. Использование append

Поведение append зависит от ёмкости (cap) слайса.

  • append без реаллокации (len < cap): Новый элемент добавляется в базовый массив, используя доступную ёмкость. Это может "затереть" данные, видимые другим слайсам, которые ссылаются на ту же область памяти.

    original := []int{1, 2, 3, 4} // len=4, cap=4
    sliceA := original[:2]       // len=2, cap=4
    
    sliceA = append(sliceA, 99)  // Реаллокации нет, т.к. cap=4
    
    fmt.Println(sliceA)   // Вывод: [1, 2, 99]
    fmt.Println(original) // Вывод: [1, 2, 99, 4] // original был изменен!
  • append с реаллокацией (len == cap): Если ёмкости не хватает, Go выделяет новый, больший массив, копирует в него старые элементы, добавляет новый и возвращает слайс, указывающий уже на этот новый массив. Оригинальный массив и другие слайсы остаются нетронутыми.

    original := []int{1, 2}
    sliceA := original
    
    sliceA = append(sliceA, 3) // Произошла реаллокация
    sliceA[0] = 99             // Меняем элемент в НОВОМ массиве
    
    fmt.Println(sliceA)   // Вывод: [99, 2, 3]
    fmt.Println(original) // Вывод: [1, 2] // original не изменился

Как безопасно мутировать:

  • Для изоляции: Если нужно изменить слайс, не затрагивая оригинал, создайте полную копию с помощью функции copy().
  • Контроль append: Всегда переприсваивайте результат append той же переменной (slice = append(slice, ...)) , так как она может вернуть указатель на новый массив.
  • Конкурентность: При доступе к одному слайсу из нескольких горутин используйте мьютексы (sync.Mutex) для предотвращения гонок данных.