Разберем один классический пример, который наглядно демонстрирует важные особенности внутренней работы срезов в языке 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")
Это самый интересный момент. Давайте разберем, что происходит:
- Проверка емкости: Текущая емкость
bравна 3, а длина равна 1. - Достаточно ли метса?: Да, для добавления одного элемента достаточно (1 + 1 ≤ 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.
Поделитесь в комментариях, сталкивались ли вы с подобными «сюрпризами» в продакшн коде?