Slices.Clone в Go: копия среза, общий массив и вложенные значения

Slices.Clone в Go: копия среза, общий массив и вложенные значения
Содержание статьи

Присваивание среза в Go копирует его описание, а не все элементы базового массива. Два среза могут указывать на одни данные. slices.Clone из стандартного пакета slices создаёт копию элементов; пакет доступен начиная с Go 1.21. Это поверхностное копирование: вложенные срезы, карты и указатели требуют отдельного внимания.

В практическом разборе Mentoring.digital сначала изменим обычный срез чисел, затем проверим вложенные данные. Такой порядок помогает отличить независимость внешнего массива от глубокой копии всей структуры.

Что происходит при обычном присваивании

package main

import (
    "fmt"
    "slices"
)

func main() {
    source := []int{10, 20, 30}
    alias := source
    alias[0] = 99
    fmt.Println(source) // [99 20 30]

    independent := slices.Clone(source)
    independent[0] = 7
    fmt.Println(source)      // [99 20 30]
    fmt.Println(independent) // [7 20 30]
}

Первая запись меняет общий массив. Во второй части Clone отделяет элементы, поэтому изменение числа в копии не отражается в source. Подсрез вроде source[:2] сам по себе тоже не копирует массив.

Не используйте append как способ гарантированно отделить данные. При наличии свободной ёмкости он способен использовать тот же базовый массив, а при её нехватке выделит другой. Поведение одной конкретной попытки не доказывает независимость срезов в остальных условиях.

Почему вложенный срез остаётся общим

rows := [][]int{{1, 2}, {3, 4}}
outerCopy := slices.Clone(rows)
outerCopy[0][0] = 9
fmt.Println(rows[0][0]) // 9

deepRows := make([][]int, len(rows))
for i, row := range rows {
    deepRows[i] = slices.Clone(row)
}
deepRows[0][0] = 5
fmt.Println(rows[0][0]) // 9

Здесь скопированы элементы внешнего среза, а каждый элемент сам является описанием внутреннего среза. Поэтому внутренние массивы первоначально общие. Цикл создаёт отдельную копию каждой строки чисел. Для структуры с картами, указателями или несколькими уровнями вложенности нужно определить свой алгоритм копирования, а не считать этот цикл универсальным.

Чем отличается встроенная функция copy

source := []int{10, 20, 30}
destination := make([]int, len(source))
n := copy(destination, source)
fmt.Println(n, destination) // 3 [10 20 30]

emptyLength := make([]int, 0, len(source))
fmt.Println(copy(emptyLength, source)) // 0

copy переносит столько элементов, сколько позволяет меньшая из длин источника и назначения. Одна ёмкость без длины не создаёт доступных элементов назначения. Это распространённая причина неожиданного нуля в результате.

slices.Clone сохраняет nil: копия nil-среза тоже равна nil. Если это часть вашего контракта сериализации или API, проверьте её явно. Не подменяйте проверку nil проверкой длины: и nil-срез, и инициализированный пустой срез могут иметь len == 0.

Как проверить границы копирования

Создайте nil-срез, пустой срез, срез чисел и срез с вложенной строкой. После копирования измените внешний элемент, затем вложенный. Для каждого случая заранее сформулируйте, должен ли измениться источник. Если копирование нужно для конкурентного доступа, отдельно разберите владение вложенными данными: поверхностная копия не делает общую карту или указатель потокобезопасными.

Для освоения устройства данных подойдут курсы Go. Неочевидные зависимости между срезами можно разобрать с ментором по Go.

Похожие статьи по данной теме