«Если рабочий хочет хорошо выполнять свою работу, он должен сначала заточить свои инструменты» — Конфуций, «Аналитики Конфуция. Лу Лингун»
титульная страница > программирование > Как работают массивы Go и возникают сложности с For-Range

Как работают массивы Go и возникают сложности с For-Range

Опубликовано 22 августа 2024 г.
Просматривать:369

Это отрывок из сообщения; Полный пост доступен здесь: Как работают массивы Go и возникают сложности с For-Range.

Классический массив и срез Golang довольно просты. Массивы имеют фиксированный размер, а срезы являются динамическими. Но я должен вам сказать, что Go может показаться простым на первый взгляд, но под капотом у него много чего происходит.

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

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

Что такое массив?

Массивы в Go во многом похожи на массивы в других языках программирования. Они имеют фиксированный размер и хранят элементы одного типа в смежных ячейках памяти.

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

func main() {
    arr := [5]byte{0, 1, 2, 3, 4}
    println("arr", &arr)

    for i := range arr {
        println(i, &arr[i])
    }
}

// Output:
// arr 0x1400005072b
// 0 0x1400005072b
// 1 0x1400005072c
// 2 0x1400005072d
// 3 0x1400005072e
// 4 0x1400005072f

Здесь следует обратить внимание на несколько вещей:

  • Адрес массива arr совпадает с адресом первого элемента.
  • Адрес каждого элемента находится на расстоянии 1 байта друг от друга, поскольку тип нашего элемента — байт.

How Go Arrays Work and Get Tricky with For-Range

Массив [5]байт{0, 1, 2, 3, 4} в памяти

Внимательно посмотрите на изображение.

Наш стек растет вниз от более высокого адреса к более низкому, верно? На этом рисунке показано, как именно выглядит массив в стеке: от arr[4] до arr[0].

Значит ли это, что мы можем получить доступ к любому элементу массива, зная адрес первого элемента (или массива) и размер элемента? Давайте попробуем это с массивом int и небезопасным пакетом:

func main() {
    a := [3]int{99, 100, 101}

    p := unsafe.Pointer(&a[0])

    a1 := unsafe.Pointer(uintptr(p)   8)
    a2 := unsafe.Pointer(uintptr(p)   16)

    fmt.Println(*(*int)(p))
    fmt.Println(*(*int)(a1))
    fmt.Println(*(*int)(a2))
}

// Output:
// 99
// 100
// 101

Ну, мы получаем указатель на первый элемент, а затем вычисляем указатели на следующие элементы, складывая кратные размеру целого числа, которое составляет 8 байт в 64-битной архитектуре. Затем мы используем эти указатели для доступа и преобразования их обратно в значения int.

How Go Arrays Work and Get Tricky with For-Range

Массив [3]int{99, 100, 101} в памяти

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

Теперь массив типа T сам по себе не является типом, а массив с определенным размером и типом T считается типом. Вот что я имею в виду:

func main() {
    a := [5]byte{}
    b := [4]byte{}

    fmt.Printf("%T\n", a) // [5]uint8
    fmt.Printf("%T\n", b) // [4]uint8

    // cannot use b (variable of type [4]byte) as [5]byte value in assignment
    a = b 
}

Несмотря на то, что и a, и b представляют собой массивы байтов, компилятор Go рассматривает их как совершенно разные типы, формат %T проясняет это.

Вот как компилятор Go видит это внутренне (src/cmd/compile/internal/types2/array.go):

// An Array represents an array type.
type Array struct {
    len  int64
    elem Type
}

// NewArray returns a new array type for the given element type and length.
// A negative length indicates an unknown length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

Длина массива «закодирована» в самом типе, поэтому компилятор знает длину массива по его типу. Попытка присвоить массив одного размера другому или сравнить их приведет к ошибке несовпадения типов.

Литералы массива

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

var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0]

// With value, infer-length
arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// With index, infer-length
arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3]

// Combined index and value
arr4 := [5]int{1, 4: 5} // [1 0 0 0 5]
arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]

То, что мы делаем выше (за исключением первого), одновременно определяет и инициализирует их значения, что называется «составным литералом». Этот термин также используется для срезов, карт и структур.

И вот что интересно: когда мы создаем массив, содержащий менее 4 элементов, Go генерирует инструкции для помещения значений в массив одно за другим.

Итак, когда мы делаем arr := [3]int{1, 2, 3, 4}, на самом деле происходит следующее:

arr := [4]int{}
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4

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

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

"А как насчет массивов, содержащих более 4 элементов?"

Компилятор создает статическое представление массива в двоичном виде, что известно как стратегия «статической инициализации».

Это означает, что значения элементов массива хранятся в разделе двоичного файла, доступном только для чтения. Эти статические данные создаются во время компиляции, поэтому значения непосредственно внедряются в двоичный файл. Если вам интересно, как [5]int{1,2,3,4,5} выглядит в ассемблере Go:

main..stmp_1 SRODATA static size=40
    0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ................
    0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................
    0x0020 05 00 00 00 00 00 00 00                          ........

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

Наши данные хранятся в stmp_1, который представляет собой статические данные только для чтения размером 40 байт (8 байт на каждый элемент), а адрес этих данных жестко запрограммирован в двоичном формате.

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

const readonly = [5]int{1, 2, 3, 4, 5}

arr := readonly

"А как насчет массива с 5 элементами, но инициализированы только 3 из них?"

Хороший вопрос, этот литерал [5]int{1,2,3} попадает в первую категорию, где Go помещает значения в массив одно за другим.

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

Но насколько большим является «слишком большой», спросите вы.

Начиная с Go 1.23, если размер переменной, а не только массива, превышает постоянное значение MaxStackVarSize, которое в настоящее время составляет 10 МБ, оно будет считаться слишком большим для выделения стека и будет сохранено в куче.

func main() {
    a := [10 * 1024 * 1024]byte{}
    println(&a)

    b := [10*1024*1024   1]byte{}
    println(&b)
}

В этом сценарии b переместится в кучу, а a — нет.

Операции с массивами

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

func main() {
    a := [5]int{1, 2, 3}
    println(len(a)) // 5
    println(cap(a)) // 5
}

Без сомнения, емкость равна длине, но самое главное то, что мы знаем это во время компиляции, верно?

Таким образом, len(a) не имеет смысла для компилятора, поскольку это не свойство времени выполнения, компилятор Go знает значение во время компиляции.

...

Это отрывок из сообщения; Полный пост доступен здесь: Как работают массивы Go и возникают сложности с For-Range.

Заявление о выпуске Эта статья воспроизведена по адресу: https://dev.to/func25/how-go-arrays-work-and-get-tricky-with-for-range-3i9i?1. В случае нарушения прав обращайтесь по адресу Study_golang@163. .com, чтобы удалить его
Последний учебник Более>

Изучайте китайский

Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.

Copyright© 2022 湘ICP备2022001581号-3