Массив против слайса
Массив и слайс выглядят похоже — квадратные скобки, индексация, перебор циклом — но это два принципиально разных типа с разной ценой ошибки.
Массив (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), чтобы не копировать всё содержимое зря.