Русский
Русский
English
Статистика
Реклама

Перевод Советы Golang почему указатели на срезы полезны и как их игнорирование может привести к хитрым ошибкам

Сомнения

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

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

Например, в реализации 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 или написать ответ в комментариях.

Источник: habr.com
К списку статей
Опубликовано: 02.11.2020 12:06:03
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Go

Slices

Pointer

Gotcha

Ошибки

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru