Как добавить слайс в Golang

Как добавить слайс в Golang

21 октября 2022 г.

Слайсы в Go — это не то же самое, что слайсы в другом языке программирования, например, в Python. При назначении одного фрагмента другому создается только поверхностная копия фрагмента, и ее следует использовать с осторожностью, если вы хотите создать новый фрагмент из существующего фрагмента.

Введение

Язык Go, несомненно, сегодня является одним из самых популярных языков. Его легче понять и начать с ним работать, но, тем не менее, он имеет свои тонкости. Я работал над проектом и столкнулся с таким поведением.

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

Возьмите пример приведенного ниже кода Python:

#!/usr/bin/env python3

a = [0,1,2,3,4]
print(a)

array1 = a[:1]
print(array1)


array2 = a[2:]
print(array2)

array1 = array1 + array2
array1 = array1 + array2

print(array1)
print(a)

Если вы запустите приведенный выше код, вы получите следующий вывод:

  python3 main.py
[0, 1, 2, 3]
[0]
[2, 3]
[0, 2, 3, 2, 3]
[0, 1, 2, 3]

Позвольте мне объяснить, что происходит в приведенном выше коде и его вывод, если вы новичок в Python

  1. Мы создали массив a = [0,1,2,3,4]
  2. Затем мы вырезаем (извлекаем) первое значение из исходного массива a и сохраняем его в другом массиве с именем array1. После этого array1 имеет значение [0]
  3. Затем мы снова вырезаем (извлекаем) предпоследнее значение из исходного массива a и сохраняем его в другом массиве с именем array2. После этого array1 имеет значение [2,3]
  4. Затем мы объединяем массив1 и массив2 и снова сохраняем в массив1, в результате чего наше значение массив1 будет равно [0,2,3]
  5. Мы снова объединяем массив1 и массив2 и снова сохраняем в массив1. Поскольку значение array1 было изменено по сравнению с последней операцией, равной [0,2,3],. новое значение array1 становится [0,2,3,2,3], что кажется логичным, поскольку мы объединили [0,2,3] и [2,3]
  6. Затем мы печатаем окончательное значение array1, равное [0,2,3,2,3], а также исходный массив a значение, равное [0, 1, 2, 3]

То, что произошло выше, кажется логичным и понятным.

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

package main

import (
    "fmt"
)

func main(){
    slice := []int{0,1,2,3}
    fmt.Println(slice)

    newslice1 := slice[:1]
    fmt.Println(newslice1)

    newslice2 := slice[2:]
    fmt.Println(newslice2)

    newslice1 = append(newslice1, newslice2...)
    fmt.Println(newslice1)

    newslice1 = append(newslice1, newslice2...)
    fmt.Println(newslice1)

    fmt.Println(slice)

}

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

Как и в приведенном выше питоне, есть массив, вы нарезаете его, повторно нарезаете и объединяете, добавляя его к исходному фрагменту.

Теперь найдите минутку и подумайте, что будет на выходе кода. Вы можете подумать, что это то же самое, что и приведенный выше код Python, а именно:

 go run main.go
[0, 1, 2, 3]
[0]
[2, 3]
[0, 2, 3, 2, 3]
[0, 1, 2, 3]

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

 go run main.go
[0 1 2 3]
[0]
[2 3]
[0 2 3]
[0 2 3 3 3]
[0 2 3 3]

Если вы заметили результат, он отличается от того, что мы ожидаем в нашей голове, и отличается от той же реализации в Python.

Загадочным моментом является то, что даже наш исходный фрагмент был изменен, хотя на самом деле над исходным фрагментом не производилось никаких операций.

Выявление проблемы

Покопавшись в нескольких кроличьих норах, я нашел ответ. Оказывается, слайс не такой, как слайс в питоне.

Когда вы создаете срез, он на самом деле не хранит никаких данных, а представляет собой дескриптор базового массива. Слайс в ходу лучше было бы назвать заголовок слайса или переменная слайса. переменная среза — это структура данных, описывающая непрерывный раздел массива, хранящийся отдельно от самих переменных среза? Срез, который мы создали выше, не является фактическим срезом, а скорее описанием массива, который может быть представлен как (теоретически)

type slice struct {
  Length  int
  Capacity int
  firstElement *int ( or pointer to underlying array )
}

Мы знаем, что каждый срез имеет три атрибута: длина, емкость и указатель на данные.

Когда мы создаем новый фрагмент оригинала с помощью newSlice1 := slice[:1], создается новая структура данных, для которой точки на исходный фрагмент могут быть представлены как (теоретически)< /p>

type newSlice1 struct {
  Length  1
  Capacity 4
  firstElement &underlyingArray[0]
}

И снова. Когда мы создаем новый слайс оригинала, используя newSlice2 := slice[2:], создается новая структура данных, для которой точки на исходный слайс могут быть представлены как (теоретически):< /p>

type newSlice2 struct {
  Length  2
  Capacity 4
  firstElement &underlyingArray[2]
}

В обоих случаях новый созданный фрагмент указывает на тот же исходный фрагмент и имеет общие значения с исходным фрагментом, поэтому при создании newSlice1 его значение равно [0] и < code>newSlice2 равно [2,3]. Для обоих массивов длина меняется по мере того, как мы нарезаем срез, но емкость остается неизменной, т. е. 4. Для newSlice1 используется один слот и остается 3, тогда как для newSlice2 используются два слота и остается 2.

После вызова первого добавления значение newSlice1 изменяется на [0,2,3] с одним свободным слотом и длиной 3. Его структура данных может быть представлена ​​как :

type newSlice1 struct {
  Length  3
  Capacity 4
  firstElement &array[0]
}

Поскольку newSlice1 и исходный массив slice совместно используют одни и те же элементы массива, т. е. значения, поскольку оба указывают на базовый массив, значение исходного slice изменяется на [0,2,3,3], потому что мы переопределили значения из базовых массивов с индексом 0 на 3, что в конечном итоге изменило исходный фрагмент.

Вот почему после добавления к newSlice1 значение изменяется на [0 2 3 3 3] вместо [0, 2, 3, 2, 3]< /код>.

Перед добавлением:

before_append

После добавления:

Здесь вы можете видеть, что значение базового массива среза изменяется после добавления:

after_append

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

  go run main.go
Memory location of first element of slice 0xc00001c0a0 <== Same
Memory location of first element of newslice1 0xc00001c0a0 <== Same
Memory location of first element of newslice2 0xc00001c0b0
Memory location of first element of newslice1 0xc00001c0a0
Memory location of first element of newslice1 0xc000018100
Memory location of first element of slice 0xc00001c0a0

Короче говоря, go append изменяет базовый массив слайса, когда из него создаются и изменяются новые слайсы.

Решение

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

    newArray1 = append(newArray1, "items")

Если мы хотим создать новый срез путем повторного нарезки исходного массива или среза, мы должны использовать функцию copy, как показано ниже. copy выполняет глубокое копирование среза.

package main

import (
    "fmt"
)

func main(){
    slice := []int{0,1,2,3}
    fmt.Println(slice)

    newslice1 := make([]int, len(slice))
    copy(newslice1, slice)

    newslice1 = newslice1[:1]
    fmt.Println(newslice1)

    newslice2:= make([]int, len(slice))
    copy(newslice2, slice)

    newslice2 = newslice2[2:]
    fmt.Println(newslice2)

    newslice1 = append(newslice1, newslice2...)
    newslice1 = append(newslice1, newslice2...)

    fmt.Println(newslice1)
    fmt.Println(slice)

}

Теперь, когда вы запустите следующий код, вы увидите вывод, как и ожидалось, и такой же, как вывод из кода Python.

 go run main.go
[0 1 2 3]
[0]
[2 3]
[0 2 3 2 3]
[0 1 2 3]

Это снова можно проверить, проверив адрес памяти базового массива:

 go run main.go
Memory location of first element of slice 0xc00001c0a0 <== Not same
Memory location of first element of newslice1 0xc00001c0c0 <== Not same
Memory location of first element of newslice2 0xc00001c0f0
Memory location of first element of newslice1 0xc000018100
Memory location of first element of slice 0xc00001c0a0

Заключение

Такие тонкости трудно обнаружить и даже сложно отладить, работая с go in production. Я столкнулся с этим, когда работал над волонтерским проектом для компании. Надеюсь, что эта статья смогла прояснить некоторые предостережения относительно слайсов в Go.

Пожалуйста, не стесняйтесь обращаться ко мне, если у вас есть какие-либо вопросы.


Также опубликовано здесь.


Оригинал