Почему изменяется значение слайса?

Возник вопрос, связанный с изменением слайса при вызове метода структуры. В Go передается всё по значению, поэтому в методе должна быть копия слайса структуры, поэтому при любом изменении слайса внутри функции, как я понимаю, не должно быть никаких изменений для слайса самой структуры, так как работаем с копией. Однако в данном примере кода вместо вывода того же слайса получается следующее:

{[123 456 789]}
{[123 789 789]}

При изменении элемента, который как будто удаляется из слайса вывод также изменяется. Если выбрать 123, то будет следующий:

{[123 456 789]}
{[456 789 789]}

А при 789, такой:

{[123 456 789]}
{[123 456 789]}

Пример кода:

package main

import (
    "fmt"
    "slices"
)

type T struct {
    slice []string
}

type ArrayPointer struct {
    slice []string
}

func (t T) delete(elem string) {
    index := slices.Index(t.slice, elem)
    t.slice = slices.Delete(t.slice, index, index+1)
}

func main() {
    t := T{[]string{"123", "456", "789"}}
    fmt.Println(t)

    t.delete("456")

    fmt.Println(t)
}

Основной вопрос — почему вообще изменяется слайс и как связан найденный индекс элемента с выводом нового значения слайса? Полагаю, что изменяться слайс может из-за того, что он внутри также содержит указатель на память с элементами, но так до конца этот момент и не смог осознать.


Ответы (1 шт):

Автор решения: Pak Uula

Коротко методы, которые изменяют состояние объекта, нужно определять для указателя на тип: func (t *T) delete(elem string). Такие методы оперируют непосредственно с объектом, для которого вызваны. Методы func (t T) оперируют с копией объекта.

Пруф: https://go.dev/play/p/Wau88dFOsIN

{[123 456 789]}
{[123 789]}

Длинно

Слайс - это структура данных из четырёх элементов:

  • дескриптор типа элементов слайса,
  • указатель на память, где лежат элементы слайса,
  • число элементов в слайсе,
  • максимальное число элементов, которые может вместить слайс.

Ваша функция T.delete принимает аргумент типа T по значению, то есть создаётся копия t, которая передаётся в функцию.

Внутри функции delete из слайса удаляется элемент, то есть происходит вот что:

  • элементы от 0 до index-1 остаются на своих местах,
  • элементы от index+1 до конца слайса копируются на одно место вперёд,
  • число элементов в t.slice уменьшается на единицу.

Давайте рассмотрим первый случай. После удаления элемента "456" слайс t.slice в функции T.delete имеет вид []string{"123", "789"}.

Новая память не аллоцируется, так как слайс не вырастает за пределы отведённой памяти. Это означает, что третий элемент исходного слайса остался в памяти. Когда функция T.delete возвращает управление, копия t уничтожается. Тот t, который в main, остаётся без изменний - его t.slice указывает на ту же область памяти, длина осталась той же - три элемента. Но функция T.delete изменила содержимое памяти, скопировав элемент "789" на место элемента "456". Но сам элемент "789", стоявший на третьем месте в исходном слайсе, остался на своём месте. Его никто не удалял. Поэтому он остался виден в слайсе main::t.slice

Аналогично во втором случае. При удалении элемента "123" элементы "456" и "789" были скопированы на первую и вторую позицию в памяти, но третий элемент остался в памяти, и остался виден в main::t.slice, так как в этом слайсе по-прежнему три элемента.

В третьем случае ничто никуда не копировали, в T.delete::t.slice уменьшили число элементов на 1. Но так как это копия слайса из main::t, то оригинал не изменился, и элемент "789" остался виден в main.

Поменяйте метод на func (t *T) delete(elem string). Тогда slices.Delete будет оперировать непосредственно слайсом того объекта, для которого был вызван метод delete, а не с его копией.

→ Ссылка