15. Массивы и их особенности
В динамических языках программирования (Python, PHP, JavaScript) словом «массив» привыкли называть гибкую, эластичную ленту, куда можно динамически добавлять разношерстные данные и менять ее длину по щелчку пальцев. В мире C и C++ массив при передаче в функцию и в большинстве выражений неявно превращается в указатель на первый элемент непрерывного блока памяти (длина при этом теряется), и компилятор беззастенчиво разыменовывает его по правилам адресной арифметики.
В языке Go массив — это совершенно иная сущность: монолитная, строгая и статически зафиксированная по размеру конструкция. В реальном продакшен-коде на Go вы редко будете объявлять массивы напрямую, почти всегда отдавая предпочтение их динамической обертке — срезам (slices).
Однако без кристального понимания анатомии массива невозможно понять внутреннее устройство срезов, природу утечек памяти и основы оптимизации кода под архитектуру кэшей современных процессоров. Массив — это кремниевый фундамент коллекций. Давайте разберем, как он устроен на уровне физической памяти.
Длина — это часть типа
Фундаментальное правило системы типов Go звучит так: размер массива жестко зашит в его тип на этапе компиляции.
Типы [3]int и [5]int для компилятора Go являются двумя абсолютно разными, несовместимыми типами, точно так же, как несовместимы int и string. Вы не можете присвоить массив одной длины массиву другой длины или передать [3]int в аргумент функции, ожидающей [5]int:
package main
func main() {
var a [3]int
var b [5]int
// a = b // Ошибка компиляции! Cannot use 'b' (type [5]int) as type [3]int
_ = a
_ = b
}
Благодаря такой строгости компилятор на этапе сборки вычисляет точный размер структуры в байтах. Небольшой локальный массив, который не убегает из функции, он размещает во фрейме стека без единой динамической аллокации. (Явные локальные переменные больше 128 КБ и массив, на который убегает указатель, уйдут в кучу.)
Синтаксис инициализации
Go предоставляет несколько лаконичных способов создания массивов:
// Массив инициализирован нулевыми значениями (Zero Values)
var zeros [5]int
// Инициализация с явным заданием значений
primes := [4]int{2, 3, 5, 7}
// Компилятор сам посчитает длину благодаря оператору [...]
magic := [...]string{"Go", "Rust", "C++"} // Тип будет выведен как [3]string
// Инициализация по индексам (остальные элементы будут заполнены нулями 0)
sparse := [10]int{0: 42, 9: 100}
Mechanical Sympathy: Идеальная структура для CPU
С точки зрения кремния и архитектуры микропроцессоров массив в Go — это совершенная структура данных. В оперативной памяти он размещается единым непрерывным блоком байт, где элементы уложены строго друг за другом без каких-либо служебных заголовков, смещений или скрытых указателей.
Если вы объявляете массив var arr [4]int64, компилятор выделит в памяти ровно 32 байта (4 элемента по 8 байт):
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
A["arr[0]<br>(8 байт)"] --- B["arr[1]<br>(8 байт)"] --- C["arr[2]<br>(8 байт)"] --- D["arr[3]<br>(8 байт)"]
Массивы передаются по значению
В классическом C имя массива автоматически распадается (decays) до обычного указателя на его нулевой элемент. Поэтому в C вы всегда передаете в функцию легковесный 8-байтный адрес.
В Go массивы обладают семантикой значений (Value Semantics).
При присваивании массива новой переменной или при передаче его в функцию компилятор производит полную побитовую копию всего массива:
func modify(arr [3]int) {
arr[0] = 999 // Меняем КОПИЮ
}
func main() {
original := [3]int{1, 2, 3}
modify(original)
fmt.Println(original[0]) // Выведет 1, оригинал не изменился!
}
Если передача массива необходима без копирования, передают явный указатель на массив *[3]int. Язык Go избавляет от необходимости писать громоздкое разыменование (*arr)[0] — синтаксис индексатора автоматически работает по указателю:
// Передача указателя на массив
func modifyPtr(arr *[3]int) {
arr[0] = 999
// В Go не нужно писать (*arr)[0], синтаксис позволяет обращаться к индексу напрямую
}
Где массивы реально используются?
Если в 99% случаев в Go-коде применяются срезы, зачем массивы вообще сохранены в языке?
- Криптография и безопасность:
В криптографических алгоритмах размер хэша строго фиксирован математически. Функцияsha256.Sum256из стандартной библиотеки возвращает именно фиксированный массив[32]byte, а не динамический слайс. Популярная библиотека работы с UUID (github.com/google/uuid) хранит идентификатор внутри структуры как массив[16]byte. Это дает железную гарантию компилятора, что размер хэша не изменится и не будет случайно урезан в рантайме. - Нулевые аллокации (Zero Allocation):
Поскольку длина массива известна на этапе компиляции, компилятор размещает небольшой локальный массив прямо на быстром стеке горутины — если массив не убегает из функции. Временный буфер вида[4096]byteна стеке не создает ни малейшей нагрузки на сборщик мусора (Garbage Collector). Но передачаbuf[:]в метод интерфейса (io.Reader.Read,net.Conn) обычно отправляет буфер в кучу: компилятор не знает, что сделает с ним реализация. Проверяйте через-gcflags=-m. - Фундамент для слайсов:
Срез (slice) не может существовать сам по себе. Под капотом любого среза всегда располагается непрерывный базовый массив.
Итог
- Длина — часть типа: Тип
[3]intнесовместим с[5]int. Размер массива неизменяем после компиляции. - Идеальная топология памяти: Массив представляет собой монолитный блок в оперативной памяти, оптимизированный под работу аппаратного предвыборщика CPU (Hardware Prefetcher).
- Value Semantics: Передача массива означает побитовое копирование всех его элементов. Избегайте передачи больших массивов без указателей.
- Сравнение: Массивы сравниваемы через
==, в отличие от срезов. - Применение: Криптографические хэши (
[32]byte), стековые буферы (если они не убегают) для исключения аллокаций и базовый фундамент для слайсов.
Массив — это крепкий, но статичный фундамент. Чтобы дать программистам динамическую гибкость без потери производительности непрерывной памяти, создатели Go надстроили над массивами изящную структуру — слайс (slice).
В следующей лекции мы переходим к главной рабочей лошадке языка Go: 16. Slice. Главная структура данных в Go.