Язык Go — массивы
Массивы в Go — это значения, а не указатели. Разбираем чем это отличается от C, когда массивы полезны и почему в идиоматичном Go вместо них используют слайсы.
Содержание
Массивы в Go — фундамент для слайсов, но сами по себе используются редко. Прежде чем перейти к слайсам, стоит понять, чем массивы Go принципиально отличаются от массивов в C — иначе слайсы будут непонятны.
Три главных отличия от C
Массив — это значение, а не указатель. Присвоение одного массива другому копирует все элементы. Это дорого, зато предсказуемо.
Передача массива в функцию создаёт копию. Функция получает полную копию массива, а не указатель на первый элемент. Изменения внутри функции не затронут оригинал.
Размер — часть типа. [10]int и [20]int — это два разных типа. Их нельзя передать в одну и ту же функцию без приведения.
a := [3]int{1, 2, 3}
b := a // b — полная копия a, не ссылка
b[0] = 99
fmt.Println(a) // [1 2 3] — a не изменился
fmt.Println(b) // [99 2 3]
Размер как часть типа
Компилятор различает массивы разного размера — это разные типы:
var a [10]int
var b [20]int
// a = b // ошибка компиляции: cannot use b (type [20]int) as type [10]int
// функция принимает только [3]float64 — ничто другое не подойдёт
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1} // [...] — компилятор сам считает элементы, выходит [3]float64
x := Sum(&array) // явно передаём адрес, потому что Sum ждёт указатель
fmt.Println(x) // 24.6
[...] — синтаксис, при котором компилятор сам считает количество элементов из литерала. [...]float64{7.0, 8.5, 9.1} эквивалентно [3]float64{7.0, 8.5, 9.1}.
Копирование при присвоении — и когда это полезно
Семантика значения означает, что при присвоении вы всегда получаете независимую копию:
type Board [8][8]byte // шахматная доска: 8x8 байт
const (
Empty = '.'
Rook = 'R'
Knight = 'N'
)
// расставляем фигуры на доске
board := Board{}
for i := range board {
for j := range board[i] {
board[i][j] = Empty
}
}
board[0][0] = Rook
board[0][1] = Knight
// делаем снимок состояния — это полная независимая копия
snapshot := board
snapshot[0][0] = Empty // меняем снимок
fmt.Println(board[0][0]) // R — оригинал не затронут
fmt.Println(snapshot[0][0]) // . — снимок изменён
Это удобно, когда нужно зафиксировать состояние без риска, что кто-то его изменит.
Передача в функцию: копия или указатель
Поскольку массив — значение, передача в функцию копирует все данные. Для большого массива это дорого:
// функция получает копию — изменения не видны снаружи
func doubleAll(a [5]int) [5]int {
for i := range a {
a[i] *= 2
}
return a // возвращаем изменённую копию
}
nums := [5]int{1, 2, 3, 4, 5}
doubled := doubleAll(nums)
fmt.Println(nums) // [1 2 3 4 5] — оригинал не тронут
fmt.Println(doubled) // [2 4 6 8 10]
Если нужна C-подобная эффективность — передаём указатель явно:
// функция меняет оригинал через указатель
func doubleInPlace(a *[5]int) {
for i := range a {
a[i] *= 2
}
}
nums := [5]int{1, 2, 3, 4, 5}
doubleInPlace(&nums)
fmt.Println(nums) // [2 4 6 8 10] — оригинал изменён
Но даже такой стиль — не идиоматичный Go. Для этих задач используют слайсы.
Сравнение массивов
В отличие от слайсов, массивы в Go можно сравнивать через == — если их тип совпадает:
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 — третий элемент отличается
// [3]int и [4]int — разные типы, сравнение не скомпилируется:
// d := [4]int{1, 2, 3, 4}
// fmt.Println(a == d) // ошибка компиляции
Это позволяет использовать массивы как ключи в map — слайсы так не умеют:
// массив как ключ map — работает, потому что массивы сравнимы
visited := map[[2]int]bool{}
visited[[2]int{0, 0}] = true
visited[[2]int{1, 3}] = true
fmt.Println(visited[[2]int{0, 0}]) // true
fmt.Println(visited[[2]int{2, 2}]) // false — не посещали
// слайс ключом быть не может:
// bad := map[[]int]bool{} // ошибка компиляции: slice is not comparable
Многомерные массивы
Go поддерживает многомерные массивы через вложение:
// матрица умножения: таблица умножения от 1 до 4
var table [4][4]int
for i := range table {
for j := range table[i] {
table[i][j] = (i + 1) * (j + 1)
}
}
// table[2][3] == 3 * 4 == 12
fmt.Println(table[2][3]) // 12
// составной литерал для многомерного массива
identity := [3][3]int{
{1, 0, 0}, // строка 0
{0, 1, 0}, // строка 1
{0, 0, 1}, // строка 2
}
fmt.Println(identity[1][1]) // 1
Где массивы реально полезны
Массивы оправданы в нескольких конкретных ситуациях.
Фиксированный протокол или формат данных. Если структура данных имеет строго заданный размер — массив лучше слайса, потому что размер закреплён на уровне типа:
type IPv4 [4]byte
type MAC [6]byte
addr := IPv4{192, 168, 1, 1}
mac := MAC{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
fmt.Printf("%d.%d.%d.%d\n", addr[0], addr[1], addr[2], addr[3]) // 192.168.1.1
Ключи в map. Как показано выше — слайс ключом быть не может, а массив может.
Избежание лишней аллокации. Небольшой массив на стеке дешевле, чем слайс с аллокацией в куче. Компилятор Go часто сам принимает это решение, но явный массив фиксированного размера помогает:
// хеш SHA-256 всегда 32 байта — массив точнее слайса
import "crypto/sha256"
data := []byte("hello")
hash := sha256.Sum256(data) // возвращает [32]byte, не []byte
fmt.Printf("%x\n", hash)
Итоги
- Массив в Go — значение, а не указатель. Присвоение и передача в функцию копируют все элементы
- Размер — часть типа:
[10]intи[20]intнесовместимы [...]T{...}— компилятор сам считает размер из литерала- Массивы можно сравнивать через
==и использовать как ключи в map — слайсы так не умеют - Передача через указатель (
*[N]T) даёт C-подобную эффективность, но это не идиоматичный Go - В большинстве задач используйте слайсы — массивы нужны для фиксированных структур, ключей map и точного контроля над памятью