Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 16 из 25 ~15 мин +50 XP

Массивы

Массивы go — это коллекции с фиксированной длиной, зашитой прямо в тип: [3]int и [4]int — два разных типа, и присвоить один другому не выйдет. В этом уроке разберём литералы (полный, короткий [...]int и индексированный), нулевые значения, перебор, копирование при присваивании, сравнение через == и сетки [2][3]int — а в конце честно ответим, почему в прикладном коде массивы встречаются редко и что занимает их место.

Содержание урока · 11 разделов

Массивы go: фиксированная длина как часть типа#

Шахматная доска — 64 клетки и ни одной больше. Пиксель RGB — три канала. Хеш SHA-256 — ровно 32 байта. Когда размер коллекции известен заранее и меняться не будет, для неё в Go есть специальный тип — массив. Записывается он как [n]T, где n — количество элементов, а T — тип каждого: [3]int — три числа, [4]string — четыре строки, [32]byte — тридцать два байта.

Главное, что нужно запомнить про массивы go: фиксированная длина — часть типа. [3]int и [4]int — два разных, никак не связанных типа, как int и string; присвоить значение одного переменной другого не получится, компилятор остановит программу раньше, чем она запустится. Массив нельзя растянуть, обрезать или приписать к нему ещё один элемент — размер выбирается один раз при создании и живёт с массивом до конца.

Длину возвращает функция len(): для массива это константа, известная уже из типа. Начинка по умолчанию — нулевая: свежий [3]int — это три нуля, свежий [2]string — две пустые строки. Память под массив выделяется вся сразу, одним куском, без перестановок и перераспределений — этим массивы и берут, когда размер действительно не меняется. Посмотрим на обе формы объявления.

Две формы объявления

GO
package main

import "fmt"

func main() {
        // Нулевое значение: под все элементы уже выделены нули
        var zeros [3]int
        var names = [3]string{"анна", "борис", "вера"}

        fmt.Println(zeros, len(zeros))
        fmt.Println(names, len(names))
}

На настоящем Go программа напечатает «[0 0 0] 3» и «[анна борис вера] 3»: форма var zeros [3]int без литерала создаёт массив из нулей — это его нулевое значение. Блок помечен как незапускаемый из-за честной оговорки: запасной TS-интерпретатор песочницы (он же проверочный в probe-скриптах) форму var без литерала понимает неправильно и делает массив нулевой длины. Основной движок песочницы — настоящий Go — покажет три нуля. Все ранимые примеры урока поэтому объявляют массивы литералами: для них оба движка совпадают.

Песочница: первый массив

Запустите: неделя напечатана одной строкой, длина семь, а «четверг» — это индекс 3, потому что счёт идёт с нуля. Поменяйте temps[6] = 25 на temps[0] = 25 и сравните вывод: нулевой индекс и есть понедельник. А теперь попробуйте прочитать temps[7] — индексы кончаются на шести, и песочница ответит ошибкой «индекс за границами» с указанием длины: та же ошибка, что у срезов из прошлых уроков. Заметьте и литерал: семь элементов для [7]int — ровно в полного размера; формы с пропусками разберём ниже.

Вывод

Запустите код, чтобы увидеть вывод.

Проверьте себя: разные типы

В программе написано: a := [3]int{1, 2, 3}, а следом var b [4]int = a. Что скажет настоящий Go?

Литералы массива: полный, короткий и [...]int#

Массив из одних нулей нужен редко, поэтому массив почти всегда создают с литералом — перечислением значений в фигурных скобках. Полная форма повторяет тип: [3]int{5, 0, 9} — три элемента, все на местах. Короткая форма [...]int доверяет подсчёт компилятору: [...]int{40, 25, 17} — это тоже [3]int, только тройку выписывать не пришлось; допишете четвёртое значение — длина пересчитается сама. Для таблиц-констант, состав которых ещё дрожит, это удобно: тип в итоге всё равно конкретный, [4]int, а не «что-то с тремя точками».

Есть и две формы с нулями. Индексированный литерал назначает значения по адресам: [4]int{0: 100, 2: 200} кладёт 100 в ячейку 0, 200 — в ячейку 2, а ячейки 1 и 3 остаются нулями. Частичный литерал [4]int{1, 2} заполняет первые ячейки подряд, а хвост добивает нулями. Обе формы — редкие гости в прикладном коде, но для таблиц с пропусками (коды ошибок, номера клавиш) пригождаются. И важная договорённость на будущее: нули на пропусках — не ошибка, а правило языка; недостающие значения считаются заданными нулевым значением своего типа.

Четыре формы литерала

GO
package main

import "fmt"

func main() {
        full := [3]int{5, 0, 9}
        short := [...]string{"go", "rust", "python"}
        sparse := [4]int{0: 100, 2: 200} // индексы 0 и 2, пропуски — нули
        partial := [4]int{1, 2}          // начало подряд, хвост — нули

        fmt.Println(full, len(full))
        fmt.Println(short, len(short))
        fmt.Println(sparse, len(sparse))
        fmt.Println(partial, len(partial))
}

Настоящий Go напечатает «[5 0 9] 3», «[go rust python] 3», «[100 0 200 0] 4» и «[1 2 0 0] 4». Запускать здесь нечего: индексированная и частичная формы в запасном TS-интерпретаторе разбираются без учёта объявленной длины — получаются «[100 200]» и «[1 2]», то есть вывод разъезжается с настоящим Go. Первые две формы — полная и с [...] — в обоих движках одинаковы, и только они используются во всех ранимых примерах этого урока. На локальном компиляторе все четыре строки дадут то, что написано здесь.

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

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        a := [4]int{3, 1, 4, 1}
        b := a
        b[0] = 100
        fmt.Println(a[0] + b[0])
}

Массив — значение: копирование при присваивании#

Вот привычка, от которой придётся отучиться, если вы пришли из Python или JavaScript: в Go массив — это значение, а не ссылка. Присваивание b := a копирует все элементы в новый массив — не заголовок, не указатель, а всю коробку целиком. Поменяли что-то в b — a осталось прежним. То же при передаче в функцию: параметр-массив получает собственную копию, и сколько бы функция ни старалась, оригинал она не тронет. Это родственно факту из урока про range, где переменная v тоже была копией элемента — тут копируется уже весь массив разом.

Для маленьких массивов поведение только в радость: данные в безопасности, никаких сюрпризов с общим состоянием. Но копировать массив из миллиона элементов лишь ради того, чтобы передать его в функцию, — расточительство, и настоящий код так не делает. Именно поэтому в сигнатурах функций массивы больших размеров не водятся: передают срезы — их копия занимает три машинных слова, сколько бы данных за ними ни стояло. Срезам посвящён следующий урок; а сейчас убедимся в копировании руками.

Песочница: копия или ссылка

Запустите и прочитайте все четыре строки: backup получил полную копию, поэтому правка второго элемента не задела оригинал, а функция raise меняла свою собственную копию. Теперь эксперимент: замените [3]int на []int в двух местах — в объявлении original и в параметре функции — и запустите снова. Правки полезут в оригинал: срез при присваивании копирует не данные, а ссылку на них. Запомните этот контраст — массив значение, срез окно в данные; детали в следующем уроке.

Вывод

Запустите код, чтобы увидеть вывод.

Проверьте себя: тип литерала

Какого типа выражение [...]int{7, 8, 9, 1}?

Перебор массива: for и range#

Перебирается массив теми же циклами, что и срез. Классический for со счётчиком и len() — из урока про цикл for: аккумулятор объявляется до цикла, индекс бегёт от нуля до len(arr) не включительно, внутри читаем arr[i]. Range из прошлого урока работает с массивом так же, как со срезом: две переменные — индекс и копия значения, одна переменная — только индекс, подчёркивание пропускает ненужное. Внутри тела работают break и continue с теми же правилами, что в уроке 14.

Одно правило стоит повторить, потому что оно то и дело ловит новичков: значение v из range — копия. Присвоите v = v 10 — изменится копия, а массив останется прежним; чтобы поменять элементы, пишите по индексу — arr[i] = 2. Индекс из заголовка после цикла не живёт: переменные цикла умирают вместе с ним. И ещё: перебор массива всегда конечен, вечный цикл на range не написать — границей служит длина, вшитая в тип.

Перебор на практике

GO
package main

import "fmt"

func main() {
        cups := [7]int{120, 135, 98, 140, 210, 250, 180}

        sum := 0
        for i := 0; i < len(cups); i++ {
                sum += cups[i]
        }
        fmt.Println("сумма:", sum)

        best := cups[0]
        for _, v := range cups {
                if v > best {
                        best = v
                }
        }
        fmt.Println("рекорд:", best)

        for i := range cups {
                cups[i] = cups[i] / 2
        }
        fmt.Println(cups)
}

Три перебора подряд — три типовые задачи. Первый — классический for со счётчиком и len(): аккумулятор объявлен до цикла, индекс нужен для чтения cups[i]. Второй — range с подчёркиванием: значение нужно, индекс нет; обратите внимание, поиск максимума начинается с cups[0], а не с нуля — на отрицательных числах нулевой старт дал бы неверный ответ. Третий — range с одной переменной-индексом: деление по месту меняет сам массив. Блок ранимый — прогоните и сверьте вывод.

Вставьте пропуски: сумма буфера

Допишите классический перебор массива: граница цикла по длине и печать накопленной суммы. Правильная программа печатает «сумма: 150».

buf := [5]int{10, 20, 30, 40, 50} total := 0 for i := 0; i < (buf); i++ { total += buf[i] } fmt.Println("сумма:", )

Сравнение массивов через ==#

У массивов есть роскошь, которой срезы лишены: их можно сравнивать оператором ==. Сравнение поэлементное: два массива равны, если равны их длины (а они совпадают уже из типа) и каждый элемент первого равен соответствующему элементу второго. Условие if a == b с массивами — законная и частая запись для поиска «не менялась ли конфигурация». Типы слева и справа должны совпадать до последней цифры длины: сравнить [3]int с [4]int компилятор не даст даже при одинаковых значениях.

Ограничение одно: элементы должны быть сравнимы сами по себе — числа, строки, bool, массивы из них. Массив, внутри которого срез или карта, сравнить == не выйдет. Контраст с будущим: для срезов == запрещён вовсе (кроме сравнения с nil), и равенство двух списков на срезах приходится писать циклом руками. На массивах та же мысль — одна строка. Ещё одна причина ценить массив в golang там, где размер действительно фиксирован: сравнение, копирование и передача работают без оговорок.

Что выведет код: сравнение копий

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        a := [3]int{1, 2, 3}
        b := a
        b[0]++
        fmt.Println(a == b)
}

Многомерные массивы: сетки [2][3]int#

Массив может быть элементом другого массива — так получаются таблицы. Тип [2][3]int читается справа налево: массив из двух элементов, каждый из которых — массив из трёх int. Литерал повторяет вложенность: две строки по три числа в фигурных скобках. Индексация двумя парами скобок — grid[строка][колонка]; len(grid) вернёт число строк, а len(grid[0]) — длину первой строки, то есть число колонок.

Перебор — вложенный range: внешняя пара отдаёт строку целиком (это тоже копия — массив [3]int), внутренняя гуляет по её элементам. В памяти многомерный массив — один сплошной кусок: строки лежат подряд, без указателей между ними, поэтому для игровых полей и матриц это дёшево и предсказуемо. Типичная запись board[0][0] — левый верхний угол; изменение grid[0][1] = 9 трогает только одну ячейку и ни одну строку больше. Проверим всё это в песочнице.

Песочница: сетка два на три

Запустите: сетка печатается строка за строкой, len(grid) считает строки, len(grid[0]) — колонки. Обратитесь к левому столбцу: grid[0][0] и grid[1][0]. Затем замените литерал на [3][3]int{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}} — единичную матрицу — и допишите печать следа: grid[0][0] + grid[1][1] + grid[2][2], должно выйти 1. Обратите внимание: внутренняя переменная row в range — это [3]int, и её печать даёт обычную строку в скобках.

Вывод

Запустите код, чтобы увидеть вывод.

Когда массив в golang уместен: буферы, хеши, сетки#

Пришло время честного разговора. В прикладном коде на Go вы почти не встретите массивов в сигнатурах и переменных — их место заняли срезы: они растут через append, копируются дёшево и дружат со всеми коллекциями языка. Правило большого пальца: в 95% случаев вам нужен срез, и следующий урок курса — про них; полный план тем — на странице курса go-basics. Но есть ниши, где массив в golang остаётся правильным выбором, и их полезно знать в лицо.

Первая — криптография: хеши живут в массивах фиксированной длины, [16]byte у MD5 и [32]byte у SHA-256; стандартная библиотека объявляет дайджесты именно массивами, потому что размер хеша — часть его смысла. Вторая — буферы известного размера: кольцевой буфер на 64 сообщения, таблица кодов клавиш, палитра из 256 цветов — там, где ёмкость определена задачей, а не данными. Третья — сетки и координаты: доска [8][8], поле игры, матрица преобразования. Четвёртая — таблицы-константы уровня пакета, где литерал с [...] читается как «ровно столько значений, сколько написано».

И последнее, ради чего массивы стоит понимать заранее: срезы устроены поверх них. Срез — это окно в чужой массив: маленький заголовок с указателем, длиной и ёмкостью, глядящий на непрерывный кусок памяти. Когда в следующем уроке срез вдруг «поделится» данными с другим срезом, вспомните массивы-значения — и магии не останется.

Шпаргалка: операции с массивами#

ЗаписьЧто делаетВ песочнице
a := [3]int{5, 0, 9}полный литерал: массив из трёх чиселработает
a := [...]int{5, 0, 9}длину считает компилятор, тип — [3]intработает
var a [3]intнулевое значение: три нулязапасной TS-движок даёт длину 0 — проверяйте на настоящем Go
a[2] = 30запись по индексу, индексы от 0 до len-1работает
len(a)длина — константа, часть типаработает
b := aкопия всех элементов, оригинал целработает
a == bпоэлементное сравнение одинаковых типовработает
grid := [2][3]int{...}сетка: массив массивовработает
[4]int{1, 2}частичный литерал, хвост — нулизапасной TS-движок теряет нули — проверяйте на настоящем Go

Соберите программу: итог по размерам

Соберите программу: короткий литерал [...]int, range с подчёркиванием, аккумулятор до цикла и две итоговые строки — «итого: 82» и «элементов: 3».

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: массивы

Термин

Что означает тип [5]string и чем он отличается от []string?

нажмите, чтобы перевернуть
Определение

[5]string — массив ровно из пяти строк, длина — часть типа; []string — срез, размер не фиксирован. [5]string и [6]string — разные типы, присвоить один другому нельзя.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Массив — коллекция с фиксированной длиной, вшитой в тип: [3]int и [4]int — разные типы, растянуть или обрезать массив нельзя.
  • Литералы: полный [3]int{5, 0, 9}, короткий [...]int с подсчётом длины компилятором, индексированный и частичный — с нулями на пропусках.
  • Массив — значение: присваивание и передача в функцию копируют все элементы, оригинал в безопасности.
  • Перебор — классическим for с len() или for range: одна переменная — индекс, две — индекс и копия значения.
  • Массивы сравнимы через == поэлементно; типы слева и справа должны совпадать до последней цифры длины.
  • Многомерный [2][3]int — массив массивов: индекс двумя парами скобок, перебор вложенным range, память — один кусок.
  • В прикладном коде массивы редки — срезы закрывают 95% задач; массивы живут в хешах [32]byte, фиксированных буферах и сетках.

Домашнее задание

Соберите недельный отчёт кофейни: days := [7]string{"пн", "вт", "ср", "чт", "пт", "сб", "вс"} и cups := [7]int{120, 135, 98, 140, 210, 250, 180}. Классическим for с len() посчитайте сумму чашек и напечатайте «итого: 1133». Вторым циклом, range-ом, найдите рекордный день: сравнивайте v с рекордом и запоминайте индекс i, в конце напечатайте days[i] и cups[i] — должно выйти «рекорд: сб 250». Затем сделайте копию planned := cups, положите в её субботнюю ячейку 0 и напечатайте cups — оригинал останется целым: массив копируется целиком.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Base · Массивы)

Вопросы и ответы по уроку

Чем массив отличается от среза?

Длина массива фиксирована и входит в тип, срез растёт и ужимается. Массив — значение: присваивание копирует все элементы; срез копирует маленький заголовок, указывая на те же данные. Массивы сравниваются через ==, для срезов сравнение запрещено. Срез — надстройка над массивом: он смотрит окном в непрерывный кусок памяти.

Почему [3]int и [4]int — разные типы?

Потому что длина — часть типа: компилятор по типу знает точный размер в памяти и проверяет каждую передачу массива между переменными, параметрами и возвратами. Присвоить [3]int переменной типа [4]int нельзя — программа просто не скомпилируется. Это цена и выгода фиксированной длины: меньше сюрпризов в рантайме, больше проверок на старте.

Зачем учить массивы, если в коде почти всегда срезы?

Три причины. Первая: ниши, где массив — правильный выбор, это хеши [32]byte, буферы фиксированного размера и сетки. Вторая: срезы устроены поверх массивов, и без понимания массивов-значений поведение срезов выглядит магией. Третья: range, индексация и границы у них общие — всё, что вы отработали на массивах здесь, переносится на срезы дословно.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления