Срез Go испортил исходный список: где нужна slices.Clone
Обработчик получает список выбранных товаров, берёт первые два и меняет название для превью. После этого исходный каталог тоже содержит изменённое название. Код не использовал указатель на весь список, однако slice уже представляет окно в массив. Создание другого окна не копирует элементы этого массива.
Увидеть общую память
Значение slice содержит сведения о базовом массиве, длине и вместимости. Присваивание или передача в функцию копирует это описание. Два slice могут указывать на одни и те же элементы, поэтому запись через индекс одного видна через другой. Чтобы преобразование результата не меняло владельца входа, создайте отдельный массив и скопируйте элементы.
slices.Clone делает такую верхнюю копию читаемо и возвращает slice того же типа. В примере сначала демонстрируется общая запись, затем исходное состояние восстанавливается и проверяется независимость клона. Это важнее теста на равенство сразу после копирования: равные элементы ещё не говорят, разделяют ли коллекции память.
Но Clone копирует сами элементы, а не весь достижимый граф объектов. Если элемент содержит map или другой slice, соответствующие внутренние объекты остаются общими. Поэтому пример отдельно показывает структуру с картой меток. Изменение поля Name в клоне изолировано, изменение map отражается в оригинале. Обе проверки определяют точную границу копии.
Проверить границу Clone
package main
import (
"fmt"
"slices"
)
type Item struct {
Name string
Labels map[string]string
}
func main() {
original := []Item{{Name: "base", Labels: map[string]string{"color": "red"}}}
view := original[:1]
view[0].Name = "shared"
if original[0].Name != "shared" {
panic("expected shared backing array")
}
original[0].Name = "base"
clone := slices.Clone(original)
clone[0].Name = "preview"
if original[0].Name != "base" {
panic("top-level clone is not independent")
}
clone[0].Labels["color"] = "blue"
if original[0].Labels["color"] != "blue" {
panic("expected shared nested map")
}
fmt.Println(original[0].Name, clone[0].Name, original[0].Labels["color"])
}
Пакет slices входит в стандартную библиотеку начиная с Go 1.21. После go run main.go программа выводит base preview blue. Этот набор показывает сразу два результата: отдельные поля структур можно менять независимо, а вложенная карта всё ещё общая. Если API обещает полностью независимый snapshot, придётся копировать и карты, и другие изменяемые поля по правилам вашей модели.
Решить судьбу вложенных данных
Полное копирование не всегда нужно. Можно сделать элементы неизменяемыми после публикации и возвращать отдельный slice только для перестановки или фильтрации. Тогда разделяемые карты тоже должны оставаться неизменяемыми. Это правило следует выразить через интерфейс пакета и тесты: запрещённое изменение из другого места не становится безопасным из-за наличия Clone.
Отдельная ловушка связана с append. Пока вместимости достаточно, добавление может использовать тот же массив и затронуть соседние окна. При недостатке вместимости появится другой массив, и ошибка начинает зависеть от размеров входа. Не строите контракт независимости на случайном realloc. Если преобразование владеет своим результатом, копирование должно быть явным.
Когда данные одновременно читаются и изменяются разными goroutine, одной копии описания slice недостаточно для устранения гонки. Либо защищайте общую память, либо передавайте действительно отдельные изменяемые объекты, либо публикуйте неизменяемые версии. Срез с теми же вложенными указателями остаётся разделяемым состоянием, даже если его верхний массив новый.
Проверьте собственную функцию через изменение результата после возврата. Затем выполните обратную проверку: изменение входа не должно менять уже выданный результат, если обещан снимок. Для вложенной модели составьте список изменяемых полей и решите судьбу каждого: копируется, остаётся общим по контракту или заменяется неизменяемым значением.
Продолжить практику
В курсе микросервисов на Go есть работа с состоянием сервиса, конкурентными операциями и тестами правил. Откройте программу и продолжите от владения памятью одного процесса к границам данных между программами: там тоже важно явно определить, какая версия принадлежит одной операции.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
