Go. Структуры данных
Массивы:
как работает с памятью

Введение
Массив — это последовательность данных, записанных по порядку в непрерывном блоке памяти. В этой лекции мы разберёмся, как массивы размещаются в памяти и почему их особенности критически важны для производительности.
Как массивы размещаются в памяти
Непрерывный блок памяти
Массивы размещаются как один непрерывный блок памяти.
Для демонстрации этого используется пакет unsafe — инструмент для прямой работы с памятью. Функция unsafe. Sizeof возвращает количество байт, занимаемое значением переменной в памяти.
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    numbers := [5]int{10, 20, 30, 40, 50}
    
    fmt.Printf("Массив: %v\n", numbers)
    fmt.Printf("Размер массива: %d байт\n", unsafe.Sizeof(numbers))
    fmt.Printf("Размер одного элемента: %d байт\n", unsafe.Sizeof(numbers[0]))
    
    // Выводим адреса элементов
    fmt.Println("\nАдреса элементов:")
    for i := 0; i < len(numbers); i++ {
        fmt.Printf("numbers[%d] = %d, адрес: %p\n", i, numbers[i], &numbers[i])
    }
}
Вывод программы (пример):
Массив: [10 20 30 40 50]
Размер массива: 40 байт
Размер одного элемента: 8 байт

Адреса элементов:
numbers[0] = 10, адрес: 0xc000014078
numbers[1] = 20, адрес: 0xc000014080  ← +8 байт
numbers[2] = 30, адрес: 0xc000014088  ← +8 байт
numbers[3] = 40, адрес: 0xc000014090  ← +8 байт
numbers[4] = 50, адрес: 0xc000014098  ← +8 байт
Что здесь происходит?
  • Каждый int занимает 8 байт (на 64-битной системе).
  • Элементы расположены последовательно без пропусков.
  • Адреса идут строго с шагом в 8 байт.
  • Весь массив — это 40 байт непрерывной памяти.
Массивы на стеке vs в куче
package main

import "fmt"

// Массив остаётся на стеке
func stackArray() {
    numbers := [5]int{1, 2, 3, 4, 5}
    fmt.Printf("Сумма: %d\n", sum(numbers))
    // numbers размещён на стеке, автоматически очищается
}

// Массив "убегает" в кучу
func heapArray() *[5]int {
    numbers := [5]int{1, 2, 3, 4, 5}
    return &numbers // ← данные "убегают", размещаются в куче
}

func sum(arr [5]int) int {
    total := 0
    for _, v := range arr {
        total += v
    }
    return total
}

func main() {
    stackArray()
    
    ptr := heapArray()
    fmt.Printf("Массив в куче: %v\n", *ptr)
}
Важно

Массивы обычно размещаются на стеке, если только они не «убегают» из функции.
Передача массивов в функции
Массивы передаются по значению
В отличие от многих языков, в Go массивы передаются по значению (копируются полностью).
package main

import "fmt"

func modifyArray(arr [5]int) {
    arr[0] = 999 // изменяем копию
    fmt.Printf("Внутри функции: %v\n", arr)
}

func main() {
    numbers := [5]int{1, 2, 3, 4, 5}
    
    fmt.Printf("До вызова: %v\n", numbers)
    modifyArray(numbers)
    fmt.Printf("После вызова: %v\n", numbers) // не изменился!
}
Вывод:
До вызова: [1 2 3 4 5]
Внутри функции: [999 2 3 4 5]
После вызова: [1 2 3 4 5]  ← оригинал не изменился
Передача через указатель
package main

import "fmt"

func modifyArray(arr *[5]int) {
    arr[0] = 999 // изменяем оригинал
    fmt.Printf("Внутри функции: %v\n", *arr)
}

func main() {
    numbers := [5]int{1, 2, 3, 4, 5}
    
    fmt.Printf("До вызова: %v\n", numbers)
    modifyArray(&numbers)
    fmt.Printf("После вызова: %v\n", numbers) // изменился!
}
Вывод:
До вызова: [1 2 3 4 5]
Внутри функции: [999 2 3 4 5]
После вызова: [999 2 3 4 5]  ← оригинал изменился
Стоимость копирования
Маленькие массивы (< 1KB) копируются быстро и их можно безопасно передавать по значению. Но для больших массивов лучше использовать указатели.
package main

import (
    "fmt"
    "time"
)

// Большой массив
type LargeArray [10000]int

func processByValue(arr LargeArray) int {
    // arr скопирован целиком (80KB)
    return arr[0]
}

func processByPointer(arr *LargeArray) int {
    // передан только указатель (8 байт)
    return arr[0]
}

func main() {
    large := LargeArray{}
    
    // Копирование большого массива
    start := time.Now()
    for i := 0; i < 100000; i++ {
        _ = processByValue(large)
    }
    fmt.Printf("По значению: %v\n", time.Since(start))
    
    // Передача указателя
    start = time.Now()
    for i := 0; i < 100000; i++ {
        _ = processByPointer(&large)
    }
    fmt.Printf("По указателю: %v\n", time.Since(start))
}
Результаты (типичные):
По значению: 150ms   (копируется 80KB × 100,000 раз)
По указателю: 2ms    (передаётся 8 байт × 100,000 раз)
Разница в 75 раз! Для больших массивов всегда используйте указатели.
Когда использовать массивы
Хорошие сценарии для массивов:
  • Маленькие фиксированные структуры (координаты, RGB-цвета, небольшие матрицы)
  • Буферы фиксированного размера (кольцевые буферы, кэши)
  • Критична производительность и размещение на стеке
  • Размер известен на этапе компиляции
Плохие сценарии для массивов:
  • Размер неизвестен или может меняться
  • Большие объемы данных (> 10KB)
  • Частая передача между функциями
  • Нужна гибкость (append, изменение размера)
Многомерные массивы в памяти
Как размещаются в памяти
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // Двумерный массив 3×4
    matrix := [3][4]int{
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12},
    }
    
    fmt.Printf("Размер массива: %d байт\n", unsafe.Sizeof(matrix))
    fmt.Println("\nАдреса элементов:")
    
    for i := 0; i < 3; i++ {
        for j := 0; j < 4; j++ {
            fmt.Printf("matrix[%d][%d] = %2d, адрес: %p\n", 
                i, j, matrix[i][j], &matrix[i][j])
        }
    }
}
Важно

Многомерный массив размещается как одномерный непрерывный блок памяти по строкам (row-major order).
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
 └─── row 0 ─┘ └─── row 1 ─┘ └─── row 2 ─┘
Заключение
Теперь вы понимаете, как массивы работают с памятью в Go. Давайте подведём итоги.
Ключевые моменты:
  • Массив 
    Это непрерывный блок памяти фиксированного размера
  • Размер массива
    Часть его типа
  • Массивы
    Обычно размещаются на стеке (быстро, без GC)
  • Массивы 
    Передаются в функции по значению (копируются полностью)
  • Для больших массивов 
    Используйте указатели
Практические рекомендации:
  • Используйте массивы для маленьких фиксированных структур (<1KB)
  • Передавайте большие массивы через указатели
  • Используйте массивы для производительности критичных операций
  • Не копируйте большие массивы без необходимости
  • Не используйте массивы, если размер может меняться
Что дальше?
Теперь вы готовы изучать следующие темы:
  • Слайсы: создание, срезы, встроенные функции (append, copy).
  • Слайсы: внутренее устройство.
  • Мапы: внутренее устройство и работа с памятью.
Поздравляю, вы только что освоили работу массивов с памятью в Go!
Это знание поможет вам принимать правильные решения при выборе между массивами и слайсами.