· 8 мин 👁 1.8k Начинающий

Язык 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 и точного контроля над памятью