Массив в Go хранит фиксированное число элементов одного типа. При присваивании он копируется целиком, а его длина входит в тип: [3]int и [4]int различаются. Слайс, или срез, даёт доступ к участку массива и ведёт себя иначе при копировании.
Если функция принимает идентификатор длиной ровно 16 байт, параметр [16]byte закрепляет это требование в типе. Для списка, который пополняется во время работы, используйте слайс. Выбор типа определяет и поведение копирования: массив копирует элементы, а слайс сохраняет доступ к общему базовому массиву.
Как создать массив
Тип массива записывают как [N]T, где N задаёт длину, а T обозначает тип элемента. Длина должна быть неотрицательной целочисленной константой, представимой типом int. После создания изменить её нельзя.
Начнём с объявления и литералов:
package main
import "fmt"
func main() {
var counters [3]int
names := [3]string{"Анна", "Борис", "Вера"}
values := [5]int{10, 20}
auto := [...]int{10, 20, 30}
indexed := [...]int{2: 7, 5: 9}
fmt.Println(counters) // [0 0 0]
fmt.Println(names) // [Анна Борис Вера]
fmt.Println(values) // [10 20 0 0 0]
fmt.Println(auto) // [10 20 30]
fmt.Println(indexed) // [0 0 7 0 0 9]
}
Объявление var counters [3]int сразу создаёт массив из трёх нулей. Для строк начальное значение будет "", для указателей и слайсов внутри массива будет nil. Неуказанные элементы литерала тоже получают нулевые значения своего типа.
Запись [...]int просит компилятор вывести длину из литерала. В auto она равна трём. В indexed наибольший индекс равен пяти, поэтому длина равна шести. Многоточие здесь не делает массив динамическим: после вывода типа auto имеет тип [3]int, а indexed имеет тип [6]int.
Если длина определяется во время выполнения, нужен слайс:
// Фрагмент внутри функции. Значение n приходит во время выполнения.
n := 3
values := make([]int, n)
_ = values
// var fixed [n]int // Не компилируется: n не константа.
make создаёт слайсы, карты и каналы. Для массива эта функция не подходит. Если нужен указатель на массив с нулевыми элементами, можно написать new([3]int). Результат имеет тип *[3]int. Для обычной локальной переменной достаточно var a [3]int.
Длина входит в тип
У двух массивов должны совпадать длина и тип элемента, чтобы их неназванные типы были идентичны. Поэтому массив из четырёх чисел нельзя присвоить переменной типа [3]int:
// Фрагмент внутри функции.
a := [3]int{1, 2, 3}
b := [4]int{1, 2, 3, 4}
// a = b // Не компилируется: разные типы массивов.
fmt.Println(len(a), len(b)) // 3 4
Так функция может проверять размер данных уже по типу аргумента. Например, [16]byte позволяет выразить требование «ровно 16 байт» в сигнатуре функции. Для []byte длину пришлось бы проверять отдельно.
Константу можно вынести в имя: const size = 16, затем объявить var id [size]byte. Обычная переменная с тем же значением для длины массива не подойдёт.
Индексы, len, cap и обход
Индексы начинаются с нуля. У массива [3]int доступны элементы с индексами 0, 1 и 2. Функции len и cap для массива возвращают одно и то же число: его длину.
package main
import "fmt"
func main() {
a := [3]int{10, 20, 30}
a[1] = 25
fmt.Println(a[0]) // 10
fmt.Println(len(a), cap(a)) // 3 3
for i, value := range a {
fmt.Println(i, value)
}
}
Если константный индекс выходит за границы, компилятор отклонит программу. Например, a[3] для [3]int недопустим. Если индекс хранится в переменной, выход за границы вызовет панику во время выполнения. При работе с внешними данными проверяйте обе границы: 0 <= i && i < len(a).
В цикле for i, value := range a переменная value получает копию элемента. Присваивание value = 0 не изменит массив. Для изменения записывайте по индексу:
// Фрагмент внутри функции.
a := [3]int{10, 20, 30}
for i := range a {
a[i] *= 2
}
fmt.Println(a) // [20 40 60]
Есть и особенность самого обхода. В форме с индексом и значением range a обходит копию массива. Если внутри цикла менять ещё не посещённые элементы исходного a, последующие значения value останутся взятыми из копии. Когда нужны текущие значения оригинала, используйте обход по индексам и читайте a[i] в теле цикла.
Не стоит из этого делать вывод о размещении массива в памяти. Тип [N]T сам по себе не гарантирует стек. Решение о размещении зависит от того, как значение используется, и от компилятора.
Копирование массива через присваивание
Чтобы скопировать массив, достаточно присваивания:
package main
import "fmt"
func main() {
original := [3]int{10, 20, 30}
cloned := original
cloned[0] = 99
fmt.Println(original) // [10 20 30]
fmt.Println(cloned) // [99 20 30]
}
У original и cloned отдельные элементы. Для массива чисел этого достаточно, чтобы изменения одной копии не затрагивали другую. Отдельную функцию копирования вызывать не нужно.
Однако присваивание копирует значения элементов по правилам их типов. Если элемент содержит слайс, указатель или карту, данные, к которым он обращается, автоматически не дублируются:
package main
import "fmt"
func main() {
original := [2][]int{{1, 2}, {3, 4}}
cloned := original
cloned[0][0] = 99
fmt.Println(original[0]) // [99 2]
cloned[1] = []int{8, 9}
fmt.Println(original[1]) // [3 4]
fmt.Println(cloned[1]) // [8 9]
}
Массивы original и cloned здесь разные. Слайсы в первых элементах обоих массивов используют один базовый массив. Запись cloned[0][0] = 99 меняет его элемент. Присваивание cloned[1] = []int{8, 9} заменяет только элемент второго массива.
Чтобы получить независимые числа в этом примере, нужно отдельно скопировать каждый вложенный слайс. Если элемент содержит структуру со слайсом, отдельно копируйте и этот слайс, когда нужны независимые данные. Одно присваивание массива не обещает независимость всех вложенных объектов.
Передача массива в функцию
Параметр типа [3]int получает копию переданного массива. Изменение параметра не меняет переменную вызывающего кода:
package main
import "fmt"
func changeCopy(a [3]int) {
a[0] = 99
}
func changeOriginal(a *[3]int) {
a[0] = 99
}
func main() {
a := [3]int{10, 20, 30}
changeCopy(a)
fmt.Println(a) // [10 20 30]
changeOriginal(&a)
fmt.Println(a) // [99 20 30]
}
Указатель *[3]int позволяет изменить исходный массив. Go разрешает индексировать его без явной записи (*a)[0]. При этом длина остаётся частью типа: функция не примет указатель на [4]int.
Если функция должна работать с последовательностями разной длины, обычно удобнее параметр []int. Вызвать её для массива можно через a[:]. Слайс позволяет менять элементы исходного массива, но функция может использовать его только для чтения. Укажите в её документации, изменяет ли она переданные элементы. Сам тип []int не запрещает запись.
Возврат массива из функции тоже передаёт значение. Семантика копирования не означает, что компилятор обязан физически копировать память при каждом таком действии: он может оптимизировать операции, сохраняя наблюдаемое поведение программы.
Чем массив отличается от слайса
Сравним [3]int и []int:
| Свойство | Массив [3]int | Слайс []int |
|---|---|---|
| Длина | Входит в тип, фиксирована | Хранится в значении слайса |
| Присваивание | Копирует элементы | Копирует описание участка массива |
| Нулевое значение | Массив нулевых элементов | nil, длина и ёмкость равны нулю |
len и cap | Всегда равны | Длина может быть меньше ёмкости |
append | Напрямую неприменим | Возвращает слайс с добавленными элементами |
Сравнение через == | Зависит от сравнимости элементов | Разрешено только сравнение с nil |
Слайс хранит ссылку на базовый массив, длину и ёмкость. При присваивании слайса копируется это описание. Элементы остаются общими. Поэтому две переменные типа []int могут показывать одни и те же данные, даже если вы больше нигде явно не используете массив.
Как получить слайс из массива
Выражение a[:] создаёт слайс на весь массив. Выражение a[low:high] выбирает участок, включая индекс low и исключая high:
package main
import "fmt"
func main() {
a := [5]int{10, 20, 30, 40, 50}
part := a[1:3]
fmt.Println(part) // [20 30]
fmt.Println(len(part), cap(part)) // 2 4
part[0] = 99
fmt.Println(a) // [10 99 30 40 50]
}
Длина part равна двум. Ёмкость равна четырём: от начала участка до конца массива остаются четыре элемента. Создание слайса не копирует числа. Его нулевой элемент соответствует a[1].
Для полного выражения a[low:high:max] ёмкость равна max - low. Например, у a[1:3:3] длина и ёмкость равны двум. Это позволяет ограничить участок, который доступен для расширения слайса.
Можно ли добавить элемент в массив?
Длина массива фиксирована: добавить к нему ещё один элемент нельзя. Для растущей последовательности используйте слайс и append. Вызов append(a[:], value) вернёт слайс с дополнительным элементом, но длина массива a останется прежней.
Почему append иногда меняет исходный массив
Если ёмкости достаточно, append использует существующий массив. Если недостаточно, он выделяет новый базовый массив. Результат вызова нужно сохранить:
package main
import "fmt"
func main() {
a := [4]int{10, 20, 30, 40}
s := a[:2]
s = append(s, 99)
fmt.Println(s) // [10 20 99]
fmt.Println(a) // [10 20 99 40]
limited := a[:2:2]
limited = append(limited, 77)
limited[0] = 88
fmt.Println(limited) // [88 20 77]
fmt.Println(a) // [10 20 99 40]
}
В первом вызове append перезаписывает a[2]. Длина самого массива при этом остаётся равной четырём. Меняется длина слайса s, а не размер a.
Во втором вызове ёмкость limited ограничена двумя. Для третьего элемента нужен новый массив, поэтому последующая запись limited[0] = 88 уже не затрагивает a.
Ограничение ёмкости не создаёт копию само по себе. До вызова append слайс a[:2:2] всё ещё обращается к исходным элементам. Если сразу изменить его нулевой элемент, изменится и a[0].
Как использовать copy с массивами
Встроенная функция copy копирует элементы между слайсами. Также она может копировать байты из строки в []byte. Массивы напрямую передать нельзя. Чтобы скопировать часть одного массива в другой, получите слайсы через [:]:
package main
import "fmt"
func main() {
source := [4]int{10, 20, 30, 40}
destination := [3]int{-1, -1, -1}
n := copy(destination[:], source[:])
fmt.Println(n) // 3
fmt.Println(destination) // [10 20 30]
}
copy переносит минимум из длин источника и приёмника и возвращает число скопированных элементов. Здесь четвёртое число не помещается. Если приёмник длиннее источника, оставшиеся элементы приёмника сохраняют прежние значения. Ёмкость слайса не расширяет границы копирования: учитывается именно длина.
Для двух массивов одного типа используйте обычное присваивание, когда требуется скопировать всё значение. copy удобна для участков и массивов разной длины. Перекрытие источника и приёмника разрешено, например при сдвиге элементов внутри одного массива.
Если нужен независимый слайс из массива, создайте место для элементов и скопируйте их:
// Фрагмент внутри функции.
a := [3]int{10, 20, 30}
s := make([]int, len(a))
copy(s, a[:])
s[0] = 99
fmt.Println(a) // [10 20 30]
fmt.Println(s) // [99 20 30]
Здесь числа независимы. Ограничение для вложенных слайсов и указателей остаётся тем же, что при присваивании массива: copy копирует элементы, а не все объекты, к которым они обращаются.
Сравнение и многомерные массивы
Массивы можно сравнивать через == и !=, если тип элемента сравним. Числа, строки и массивы из них подходят:
// Фрагмент внутри функции.
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
counts := map[[2]int]int{}
counts[[2]int{10, 20}]++
fmt.Println(counts[[2]int{10, 20}]) // 1
Массив из сравнимых элементов также подходит для ключа карты. Например, [2]int может обозначать пару координат. Если элементы имеют тип []int, массив сравнивать нельзя: сами слайсы несравнимы. Для элементов интерфейсного типа, например [2]any, сравнение может вызвать панику, если внутри окажется несравнимое динамическое значение.
Многомерный массив представляет собой массив массивов. Тип [2][3]int задаёт две строки по три числа:
// Фрагмент внутри функции.
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
matrix[1][2] = 99
fmt.Println(matrix) // [[1 2 3] [4 5 99]]
Обе размерности входят в тип. Присваивание cloned := matrix копирует все шесть чисел. Если же число строк и длины строк определяются входными данными, удобнее [][]int. У такого слайса строки могут иметь разную длину, а копирование внешнего слайса не копирует их содержимое.
FAQ: частые вопросы о массивах в Go
Чем массив отличается от слайса в Go?
[N]T длина входит в тип и не меняется. Присваивание копирует его элементы. Слайс []T описывает участок базового массива. При присваивании слайса элементы остаются общими, а его длину можно менять в пределах ёмкости или получать новый слайс через append.Как добавить элемент в конец массива?
a[:] и сохраните результат s := append(a[:], value). В s будет дополнительный элемент, а длина a останется прежней. Поскольку слайс на весь массив уже занимает всю его ёмкость, добавление элемента потребует нового базового массива.Как скопировать массив независимо от оригинала?
cloned := original. Если элементы содержат слайсы, карты или указатели, связанные данные останутся общими. Для полной независимости их нужно копировать отдельно. Выражение original[:] создаёт слайс на исходный массив, а не копию элементов.Почему функция не меняет переданный массив?
[3]int получает копию массива. Чтобы изменить оригинал, передайте указатель *[3]int или слайс a[:] в параметр []int. В обоих случаях функция сможет записывать в исходные элементы.Почему copy не принимает массив?
copy(destination[:], source[:]). Функция скопирует минимум из длин источника и приёмника. Если нужно скопировать целиком массив того же типа, достаточно присваивания.Как удалить элемент из массива?
Итог
Выбирайте массив, когда фиксированный размер является частью данных: например, для идентификатора из 16 байт или пары координат. Для списка, который пополняется во время работы, используйте слайс.
При чтении кода сначала посмотрите на тип. Присваивание [N]T копирует элементы. Присваивание []T сохраняет доступ к общему базовому массиву. Выражение a[:] тоже оставляет элементы общими, а независимую копию можно получить через новый слайс и copy. Если элементы содержат указатели или слайсы, отдельно решите, требуется ли копировать вложенные данные.
Правила типов и операций описаны в спецификации Go: массивы, слайсы, append и copy.
Теги: