При копировании интерфейсного значения, содержащего срез, копируются ли его элементы?
Нет. При копировании интерфейсного значения копируется дескриптор среза, а не его элементы, поэтому копии обычно обращаются к одному базовому массиву. Изменение существующего элемента через одну копию видно через другую, но операция добавления может выделить новый массив и разорвать это совместное владение.
Интерфейсы в Go предназначены для работы с поведением, не привязываясь к конкретному типу. Срезы дают представление над массивом без копирования всех элементов при передаче значения.
Сочетание этих механизмов позволяет передавать разные типы через единый интерфейс, сохраняя эффективную работу со ссылочными структурами данных. Цена такой эффективности — необходимость понимать, где копируется только дескриптор, а где создаётся новый массив.
Интерфейсная переменная может содержать значение типа среза. Разработчик иногда ожидает, что присваивание интерфейса создаст независимую копию данных, и безопасно изменяет срез через одну переменную.
Это приводит к неожиданным изменениям в другом месте программы. Обратная ошибка тоже возможна: после append разработчик ожидает общего массива, но срез уже получил новое хранилище из-за нехватки вместимости.
Интерфейсное значение логически содержит динамический тип и данные этого типа. При присваивании интерфейса копируется значение, представляющее срез; сам срез состоит из указателя на базовый массив, длины и вместимости. Элементы массива при этом не копируются.
Поэтому запись существующего элемента через одну копию изменяет общий базовый массив. Однако append ведёт себя условно: если вместимости достаточно, он может использовать тот же массив; если нет, создаётся новый массив, и результат append начинает ссылаться на него.
В примере a и b содержат копии интерфейсного значения, но оба ссылаются на один массив. После append переменная s получает новый дескриптор с длиной, равной трём; исходное значение в a по-прежнему имеет длину два. Даже если append не выделил новый массив, изменение длины не изменяет длину среза, сохранённого внутри другого интерфейса.
Если нужна независимость данных, следует явно копировать элементы в новый срез. Если совместное изменение допустимо, это нужно учитывать в контракте функции; для конкурентного доступа дополнительно требуется синхронизация.
Обработчик принимает any и сохраняет полученный срез в кэш. Позже другой компонент извлекает его, изменяет элементы и вызывает append. Вариант с прямым сохранением прост и не требует лишнего выделения памяти, но кэш неожиданно меняется из-за общего базового массива.
Вариант с копированием при записи изолирует кэш и делает владение данными понятным, но увеличивает затраты на память и время. Вариант с неизменяемым соглашением эффективнее, однако требует дисциплины от всех вызывающих сторон.
Рациональное решение — копировать срез на границе владения, если кэш должен хранить снимок данных; если же это намеренно разделяемый буфер, следует явно задокументировать совместное владение и синхронизацию. Тогда результат не зависит от случайной вместимости среза.
append через первую копию?Нет. Длина является частью дескриптора среза. Даже если append использует тот же базовый массив, он возвращает новый дескриптор с новой длиной; другой интерфейсный экземпляр сохраняет прежний дескриптор.
Нет. Интерфейс копирует значение динамического типа согласно правилам этого типа. Для среза копируется дескриптор, для указателя — адрес объекта, а для структуры со срезовым полем копируется сама структура, но её срезовое поле всё равно может ссылаться на общий массив. Глубина независимого копирования определяется не интерфейсом, а операцией копирования конкретного типа.
Надёжно определить это по самому интерфейсу нельзя. После извлечения среза можно сравнить адреса элементов при ненулевой длине, но такой подход не даёт полной модели владения и не решает вопросы вместимости или будущих append. Если общность хранения важна, её следует задавать явно через структуру данных и контракт, а не выводить косвенно.