4.2 Массивы с точки зрения памяти

В Go длина массивов фиксируется как часть типа на момент компиляции.

// псевдокод ,примерно так будет выглядеть массив в памяти
var My5IntArray = Array {
	len: 5 // количество элементов
	elem: int // тип элементов
}

В примере мы определили, что у нас будет массив длиной из 5 элементов, компилятор в момент сборки «записывает» это значение.

А при выполнении кода метод len(a) просто отдает заранее записанное значение.

Чтобы понять, как это работает на практике, давайте рассмотрим работу пакета types:

package types

// https://github.com/golang/go/blob/master/src/go/types/array.go

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

// NewArray возвращает структуру Array, которая содержит тип элементов и длину a new array type for the given element type and length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

// Возвращает длину массива
// Выходное значение может быть отрицательным, это будет обозначать, что длина массива неизвестна.
func (a *Array) Len() int64 { return a.len }

// Возвращает тип элемента массива.
func (a *Array) Elem() Type { return a.elem }

// возвращает базовый тип, в нашем примере это будет [5]int
func (a *Array) Underlying() Type { return a }

// возвращает базовый тип в виде строки, в нашем примере это будет [5]int
func (a *Array) String() string   { return TypeString(a, nil) }

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

А что же тогда используется?

Возьмем следующий код:

package main

func main() {
	myArray := [3]int32{1, 2, 3}
	_ = myArray
}

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

// Соберем программу с отладочной информацие
go build -gcflags="-N -l" -o app

// дизассемблируем наш бинарник
go tool objdump app > dump.txt

В файле dump.txt найдем примерно следующий блок кода, которая отвечает за выполнение функции main в файле main.go

// находим секцию
TEXT main.main(SB) /path/to/main.go

// Нас интересует 
main.go:4
// Подготавливаем память
  0x10006746c		f800c3ff		MOVD ZR, 12(RSP)
  0x100067470		b90017ff		MOVW ZR, 20(RSP)
// Последовательно записываем значения массива
  0x100067474		d2800de0		MOVD $1, R0
  0x100067478		b9000fe0		MOVW R0, 12(RSP)
  0x10006747c		d2801bc0		MOVD $2, R0
  0x100067480		b90013e0		MOVW R0, 16(RSP)
  0x100067484		d28029a0		MOVD $3, R0
  0x100067488		b90017e0		MOVW R0, 20(RSP)

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

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

// Исходный код
package main

func main() {
    var a int64
    var b int64
    var c int32
    a = 1
    b = 2
    c = 3
}


// находим секцию
TEXT main.main(SB) /path/to/main.go
// выделение памяти отсутствуем, сразу записываем значения
main.go:4
0x10006746c		d2806120		MOVD $1, R0		
0x100067470		f90013e0		MOVD R0, 32(RSP)	
main.go:5
0x100067474		d2806f00		MOVD $2, R0		
0x100067478		f9000fe0		MOVD R0, 24(RSP)	
main.go:6
0x10006747c		d2807ce0		MOVD $3, R0		
0x100067480		f9000be0		MOVD R0, 16(RSP)

Копирование массивов при передаче в функцию

Так как в Go при передаче в функцию все значения копируются, то при передаче массива в функцию мы не изменим исходный массив.

package main

import "fmt"

func modify(a [3]int) {
	a[0] = 999
	fmt.Println(a) // [999 2 3] — копия изменилась
}

func main() {
	arr := [3]int{1, 2, 3}
	fmt.Println(arr) // [1 2 3] — начальный массив
	modify(arr)
	fmt.Println(arr) // [1 2 3] — начальный массив не изменился
}

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

Для того, чтобы мы могли изменить исходны массив, необходимо передать его по ссылке

package main

import "fmt"

func modify(a *[3]int) {
	a[0] = 999
	fmt.Println(a) // &[999 2 3] — копия изменилась
}

func main() {
	arr := [3]int{1, 2, 3}
	fmt.Println(arr) // [1 2 3] — начальный массив
	modify(&arr)
	fmt.Println(arr) // [999 2 3] — начальный массив не изменился
}

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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *