Срез 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 — через код и наглядные разборы.