Сомнения

Сегодня, пока я работал, возник хороший вопрос:

Почему во многих встроенных функциях и библиотеках часто можно увидеть в качестве аргументов указатели на срезы, ведь срезы всегда передаются по ссылке, не так ли?

Например, в реализации api-machinery Kubernetes мы можем увидеть функцию со следующей сигнатурой:

func ConvertSlicestringTostring (input *[] string, out *string, s conversion.Scope) error

И в примере очередей с приоритетом мы снова можем найти нечто подобное:

func (pq *PriorityQueue) Pop () interface {};

Разве срезы уже не являются указателями на хранимые в нем данные?

Давайте немного разберемся, используя Go-Playground, чтобы проверить поведение кода.

Во время всего объяснения я буду использовать один и тот же пример: функция, которая инициализирует срез, передает его в качестве аргумента второй функции, изменяет его, а затем печатает срез для проверки содержимого.

Принято считать, что срезы передаются по ссылке. В следующем примере, на самом деле, будут напечатаны [b, b]и [b, b] даже если срез был инициализирован как [a, a], поскольку он был изменен во время выполнения анонимной функции, и изменение видно в main.

func main() {
    slice:= []string{"a","a"}    
    func(slice []string){
        slice[0]="b";
        slice[1]="b";
        fmt.Print(slice)
    }(slice)
    fmt.Print(slice) 
}

Использование указателей приводит к тому же по сути результату:

func main() {
    slice:= []string{"a","a"}
 
    func(slice *[]string){
        (*slice)[0]="b";
        (*slice)[1]="b"; 
        fmt.Print(*slice)
    }(&slice)
    fmt.Print(slice) 
}

Этот код также напечатает [b, b]и [b, b] Поэтому передача по указателю выглядит бесполезной, и кажется, что срез все равно передается по ссылке, а содержимое изменяется в обоих случаях.

Так … почему же у этих функций такая сигнатура?

Объяснение

Вы можете примерно представить реализацию среза так:

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

При передаче среза в функцию по значению все поля копируются, и только данные могут быть изменены и доступны извне через копию указателя.

Однако имейте в виду, что если указатель будет перезаписан или изменен (из-за копирования, присвоения или добавления), никакие изменения не будут видны вне функции, более того, никакие изменения длины или емкости не будут видны для исходной функции.

Таким образом, ответ на вопрос прост, но он скрыт внутри реализации самого среза:

Почему во многих встроенных функциях и библиотеках часто можно увидеть в качестве аргументов указатели на срезы, ведь срезы всегда передаются по ссылке, не так ли?

Указатель на срез является неизменяемым в момент, когда функция собирается изменить структуру, размер или расположение среза в памяти. А эти изменения должны быть видны тому, кто вызывает функцию.

Когда мы передаем срез функции в качестве аргумента, значения среза передаются по ссылке (поскольку мы передаем копию указателя), но все метаданные, описывающие сам срез, являются просто копиями.

Мы можем изменить данные среза в анонимной функции, однако, если указатель на данные изменяется по какой-либо причине или метаданные среза изменяются, то это изменение может быть частично или вообще невидимым для внешней функции.

Например, если под срез снова происходит выделение памяти, то используется новое место в памяти; даже если значения совпадают, срез указывает на новое место, и поэтому никакие изменения значений не будут видны, поскольку срезы указывают на два разных места (указатель в копии среза был перезаписан).

Следовательно, в том же примере, но с принудительным повторным выделением среза будут напечатаны [b, b, a]и [a, a] Переместив append()ниже в место после манипулирования срезом, мы можем заметить, что поведение отличается, поскольку срез был перераспределен после манипуляции значениями, а указатель все еще указывает на начальный адрес памяти.

func main() {
    slice:= []string{"a","a"}
 
    func(slice []string){
        slice= append(slice, "a")
        slice[0]="b";
        slice[1]="b"; 
        fmt.Print(slice)
    }(slice)
    fmt.Print(slice) 
}

Проверим это на коде ниже:

func main() {
    slice:= []string{"a","a"}
 
    func(slice []string){
        slice[0]="b";
        slice[1]="b"; 
        slice= append(slice, "a")
        fmt.Print(slice)
    }(slice)
    fmt.Print(slice) 
}

Он печатает [b, b, a]и [b, b]по объясненным выше причинам.

Такое поведение может привести к трудно обнаруживаемым ошибкам, поскольку результат зависит от размера исходного массива, например, следующий код:

func main() {
    slice:= make([]string, 2, 3) 
    func(slice []string){        
        slice= append(slice, "a")        
        slice[0]="b";
        slice[1]="b"; 
        fmt.Print(slice)
    }(slice)
    fmt.Print(slice) 
}

Напечатает [b, b, a]и [b, b] поскольку на массив больше не происходит выделение памяти и указатель остается прежним.

Однако при добавлении еще одной строки slice = append (slice, «a», «a») под массив снова выделяется память, и результатом будет [b, b, a, a]и [](пустой массив, поскольку он не был инициализирован).

Выявить такие ошибки среди сотен или тысяч строк может быть довольно сложно.

Поэтому не забывайте, что вы можете передавать срез по значению, если хотите изменить только значения элементов, а не их количество или порядок, иначе время от времени будут возникать странные ошибки.

Теперь вы готовы понять, что выведет следующий код:

func main() {
    slice:= make([]string, 1, 3)
  
    func(slice []string){
        slice=slice[1:3]
        slice[0]="b"
        slice[1]="b"
        fmt.Print(len(slice))
        fmt.Print(slice)
    }(slice)
    fmt.Print(len(slice)) 
    fmt.Print(slice)
}

Можете запустить код на GoPlayground или написать ответ в комментариях.