Рассмотрим простой пример на Go, который демонстрирует ключевую особенность работы срезов (slices):
package main
import "fmt"
func main() {
var foo []int
var bar []int
foo = append(foo, 1)
foo = append(foo, 2)
foo = append(foo, 3)
bar = append(foo, 4)
foo = append(foo, 5)
fmt.Println(foo, bar)
}
Что выведет эта программа? [1 2 3 5] [1 2 3 4]? Или может что-то другое? Давайте разберемся.
- Понимание срезов в Go
- Что такое срез?
- Критически важная особенность
- Пошаговый разбор примера
- Шаг 1-2: Инициализация foo и bar
- Шаг 3-5: Добавление элементов в foo
- Шаг 6: Важный момент
- Шаг 7: Последнее добавление
- Результат выполнения
- Почему так происходит?
- Как избежать подобных проблем?
- 1. Использовать копирование
- 2. Использовать функцию copy
- 3. Использовать полное выражение среза
- Выводы и лучшие практики
Понимание срезов в Go
Что такое срез?
Срез (slice) в Go — это динамическая оболочка над массивом. Он состоит из 3-х основных компонентов:
- Указатель (pointer) на базовый массив
- Длина (length) — количество элементов в срезе
- Емкость (capacity) — размер базового массива
Критически важная особенность
Когда вы передаете срез в функцию append(), она проверяет, достаточно ли емкости базового массива. Если емкости недостаточно, создается новый массив, и данные копируются в него. Если емкости достаточно, используется существующий массив.
Пошаговый разбор примера
Шаг 1-2: Инициализация foo и bar
var foo []int // foo: nil, len=0, cap=0 var bar []int // bar: nil, len=0, cap=0
Шаг 3-5: Добавление элементов в foo
// создается новый массив, foo: [1], len=1, cap=1 foo = append(foo, 1) // емкости недостаточно, создается новый массив cap=2 foo = append(foo, 2) // емкости недостаточно, создается новый массив cap=4 foo = append(foo, 3) // теперь foo: [1 2 3], len=3, cap=4
Важно: у foo теперь емкость 4, хотя длина всего 3.
Шаг 6: Важный момент
bar = append(foo, 4)
Что происходит:
append(foo, 4)получает срезfoo (len=3, cap=4)- Емкость (4) больше текущей длины (3), поэтому новый массив НЕ создается
- Элемент 4 добавляется в существующий базовый массив
barтеперь указывает на тот же массив, что иfoo, но с длиной 4- Состояние:
foo: [1 2 3], len=3, cap=4, bar: [1 2 3 4], len=4, cap=4
Шаг 7: Последнее добавление
foo = append(foo, 5)
Что происходит:
append(foo, 5)получаетfoo (len=3, cap=4)- Емкости достаточно, поэтому новый массив не создается
- Элемент 5 записывается в 4-ю позицию базового массива
- Но 4-ая позиция уже содержит значение 4 (которое было добавлено через
bar) - Результат:
foo: [1 2 3 5], len=4, cap=4
Результат выполнения
Программа выведет:
[1 2 3 5] [1 2 3 5]
Не [1 2 3 5] [1 2 3 4], как можно было бы ожидать.
Почему так происходит?
Оба среза foo и bar ссылаются на один и тот же базовый массив. Когда добавляем элементы, изменяется этот общий массив.
Как избежать подобных проблем?
1. Использовать копирование
bar = append([]int{}, foo...) // создаем новый срез
bar = append(bar, 4)
2. Использовать функцию copy
bar = make([]int, len(foo)) copy(bar, foo) bar = append(bar, 4)
3. Использовать полное выражение среза
bar = append(foo[:len(foo):len(foo)], 4)
Выводы и лучшие практики
- Срезы — не массивы: они могут разделять память с другими срезами
appendможет изменять общие данные: если емкости достаточно- Для независимых копий используйте
copy(): когда нужно гарантировать разделение данных - Учитывайте емкость: понимание
capпомогает предсказать поведениеappend
Поделитесь в комментариях, сталкивались ли вы с подобными «сюрпризами» в работе со срезами?