Массив против слайса

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

Массив (array) в Go — это тип фиксированного размера: [5]int и [10]int — это два РАЗНЫХ типа, а не «массив с разным количеством элементов». Размер — часть типа, а не свойство значения, которое можно менять во время работы программы.

Что выведет этот код?

package main

func main() {
	var a [5]int
	var b [10]int

	a = b // ошибка компиляции
}

Вывод:

./main.go:6:6: cannot use b (variable of type [10]int) as [5]int value in assignment

Код даже не скомпилируется. Для Go [5]int и [10]int — это не «одинаковые массивы разной длины», а два совершенно разных типа, как int и string. Присвоить значение одного типа переменной другого типа нельзя.

Почему так строго

Сравните со слайсами: там ничего похожего не происходит — тип слайса это просто []int, длина в него не входит. Переменная []int может в разные моменты программы содержать 0, 5 или миллион элементов, оставаясь тем же типом.

С массивом наоборот: длина зашита в тип на этапе компиляции, как будто это часть имени. [5]int читается почти как отдельное слово, а не как «int, но массив». Поэтому функция, ожидающая [5]int, физически не может принять [10]int — компилятор поймает несовпадение ещё до запуска программы.

Ловушка: массив копируется целиком при передаче

Раз массив — это просто тип фиксированного размера (а не обёртка с указателем, как слайс), при передаче в функцию он ведёт себя как обычное значение, например int — копируется целиком:

package main

import "fmt"

func double(nums [3]int) {
	for i := range nums {
		nums[i] *= 2
	}
}

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

Вывод:

[1 2 3]

Функция double честно всё удвоила — но у своей личной копии массива. Снаружи arr остался нетронутым. Для сравнения — тот же код, но со слайсом вместо массива:

package main

import "fmt"

func double(nums []int) {
	for i := range nums {
		nums[i] *= 2
	}
}

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

Вывод:

[2 4 6]

Разница только в квадратных скобках ([3]int вместо []int), а поведение — противоположное. Слайс — это, как мы разбирали в прошлом уроке, обёртка с указателем на общие данные, поэтому изменения через один слайс видно и через другой, даже после «передачи по значению».

Как это работает под капотом

Ключевое отличие в том, что именно хранит переменная:

var scores [5]int          // массив: 5 int записаны прямо тут
var names []string         // слайс: указатель + len + cap (данные — отдельно)

func f() [5]int { ... }    // тип возврата содержит размер
func g() []string { ... }  // тип возврата не завязан на размер

Переменная-массив хранит сами элементы, впритык, друг за другом — это как коробка, внутри которой сразу лежат все 5 предметов. Переменная-слайс хранит не предметы, а адрес другой коробки, плюс два числа. Копирование массива — это копирование всей коробки с содержимым. Копирование слайса — это копирование бумажки с адресом чужой коробки, сама коробка остаётся одна.

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

Так когда вообще нужен массив?

Раз слайсы гибче, возникает вопрос — а массивы вообще нужны? Да, в конкретных случаях: когда размер данных — это часть смысла, а не деталь реализации. Например, дни недели (их ровно 7), координаты точки в 3D (их ровно 3), хэш фиксированной длины (например, 32 байта для SHA-256).

package main

import "fmt"

func main() {
	var week [7]string // сразу известно: ровно 7 дней, не больше и не меньше
	week[0] = "Понедельник"
	fmt.Println(len(week))

	var todo []string // список задач — сколько будет, заранее не знаем
	todo = append(todo, "Купить хлеб")
	todo = append(todo, "Сдать проект")
	fmt.Println(len(todo))
}

Вывод:

7
2

Для недели массив даже полезен: он не даст случайно записать восьмой день — такого индекса просто не существует в типе. А для списка задач заранее неизвестного размера массив был бы неудобен: пришлось бы гадать с размером наперёд.

Частые ошибки на собеседовании

  • Говорят «массив и слайс — это одно и то же, просто слайс динамический». На самом деле это разные типы с разной семантикой копирования — не просто «слайс без фиксированного размера».
  • Не могут объяснить, почему функция с параметром [5]int не примет массив длины 10 — забывают, что размер входит в тип.
  • Передают большой массив в функцию по значению «по привычке» (как передавали бы слайс) и не замечают лишнего копирования — для больших массивов правильнее передавать указатель *[N]T.
  • Путаются, почему один и тот же цикл for i := range x { x[i] *= 2 } в одной функции ничего не меняет снаружи, а в другой — меняет: смотрите на тип параметра, массив это или слайс.

Итоги-шпаргалка

  • Размер массива — часть его типа: [5]int и [10]int нельзя присвоить друг другу или передать в функцию как взаимозаменяемые.
  • Массив при передаче в функцию копируется целиком, как обычное значение — изменения внутри функции не видны снаружи.
  • Слайс — это обёртка с указателем на данные, поэтому «передача по значению» слайса всё равно даёт доступ к общим данным.
  • На практике в Go массивы используют редко и осознанно — там, где размер фиксирован по смыслу задачи. В остальных 95% случаев выбирают слайс.
  • Большой массив в функцию лучше передавать через указатель (*[N]T), чтобы не копировать всё содержимое зря.
Проверьте себя
1. Почему присвоение var a [5]int; var b [10]int; a = b не компилируется?
AПотому что Go запрещает присваивать массивы в принципе
BПотому что [5]int и [10]int — два разных типа: размер входит в тип массива
CПотому что переменные объявлены через var, а не через :=
DЭто компилируется, но выдаёт панику во время выполнения
2. В каком случае массив фиксированного размера — уместный выбор вместо слайса?
AКогда данных много и заранее неизвестно, сколько именно будет
BВсегда, потому что массивы быстрее слайсов
CКогда размер данных — часть смысла задачи и не меняется (например, 7 дней недели)
DМассивы в Go считаются устаревшими и не должны использоваться