Разберите ситуацию: два среза обращаются к одному массиву. От чего зависит, увидит ли второй срез результат append первого?
Результат append виден второму срезу, если операция записывает данные в общий массив, на который оба среза ссылаются. Если текущей ёмкости недостаточно, append выделяет новый массив, и дальнейшие изменения происходят независимо.
Даже при общем массиве второй срез увидит новые элементы только в пределах своей длины. Данные за его длиной станут видны лишь после явного расширения среза, если это допускает его ёмкость.
Срез в Go предназначен для работы с частью массива без копирования всех элементов. Поэтому он представлен небольшим дескриптором: указателем на массив, длиной и ёмкостью.
Такой подход эффективен для передачи последовательностей в функции, но означает, что копирование среза копирует только дескриптор, а не сами элементы. Это сочетает производительность с риском непреднамеренного разделения данных.
Передача или присваивание среза не создаёт независимую коллекцию. Если два среза используют один массив, запись через один из них может изменить данные, доступные через другой.
Особенно легко ошибиться при append: вызывающий код может ожидать независимость, но операция иногда изменяет общий массив, а иногда создаёт новый. Выбор зависит от текущей длины и ёмкости среза, поэтому поведение нельзя определять только по размеру добавляемых данных.
У среза есть три свойства: указатель на backing array, len и cap. При копировании среза копируется этот дескриптор, поэтому несколько срезов могут иметь общий массив.
Если после добавления новая длина не превышает cap, append записывает элементы в существующий массив и возвращает срез с обновлённой длиной. Другие срезы с тем же массивом могут увидеть эту запись.
Если ёмкости недостаточно, append выделяет новый массив, копирует туда старые элементы и добавляет новые. Возвращённый срез начинает ссылаться на новый массив, а прежние срезы остаются связаны со старым.
Длина ограничивает видимую часть массива, а ёмкость определяет, насколько далеко срез может быть расширен без нового выделения. Поэтому второй срез может пока не показывать записанный элемент, но получить к нему доступ после расширения в пределах своей ёмкости.
Здесь left и right используют один массив, а добавление помещает 9 в его второй элемент. Поэтому right видит изменение, хотя сам не изменялся.
Чтобы запретить append использовать общий запас ёмкости, применяют ограниченный срез: его максимальная ёмкость делают равной текущей длине. Тогда следующий append будет вынужден выделить новый массив. Цена этого решения — возможное дополнительное выделение и копирование.
Функция получает срез настроек, сохраняет его для последующей обработки, а вызывающий код продолжает использовать исходный срез. Если функция добавляет элементы при наличии свободной ёмкости, она может изменить общий массив, и сохранённые настройки неожиданно поменяются.
Вариант с прямым append эффективен по памяти, но создаёт скрытую связанность. Полное копирование среза обеспечивает независимость и предсказуемость, однако требует времени и памяти, пропорциональных длине данных.
Ограничение ёмкости перед передачей снижает риск случайного расширения общего массива, но не защищает от изменения уже существующих элементов. Когда владение данными должно быть независимым, предпочтительно явно копировать элементы; это даёт наиболее понятный контракт и устраняет алиасинг.
Достаточно ли сравнить длины срезов, чтобы понять, будет ли выделение памяти?
Нет. Для решения важна новая длина относительно ёмкости, а не только текущей длины. Если текущая длина равна двум, но ёмкость равна восьми, добавление нескольких элементов может использовать прежний массив.
Увидит ли другой срез добавленный элемент, если его длина меньше позиции записи?
Нет, пока его длина не включает эту позицию. Однако при достаточной ёмкости его можно расширить, после чего записанное значение станет доступно. Это не означает, что элемент был добавлен в сам второй срез: изменился общий массив, а его дескриптор остался прежним.
Гарантирует ли присваивание результата append исходному срезу отсутствие побочных изменений у других срезов?
Нет. Присваивание обновляет только дескриптор переменной, в которую помещён результат. Если append использовал общий массив, он уже мог изменить его элементы, доступные другим срезам; присваивание не отменяет эту запись.