Содержание урока · 11 разделов
- Массивы go: фиксированная длина как часть типа
- Литералы массива: полный, короткий и [...]int
- Массив — значение: копирование при присваивании
- Перебор массива: for и range
- Сравнение массивов через ==
- Многомерные массивы: сетки [2][3]int
- Когда массив в golang уместен: буферы, хеши, сетки
- Шпаргалка: операции с массивами
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Массивы 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 — две пустые строки. Память под массив выделяется вся сразу, одним куском, без перестановок и перераспределений — этим массивы и берут, когда размер действительно не меняется. Посмотрим на обе формы объявления.
Две формы объявления
GOpackage 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} заполняет первые ячейки подряд, а хвост добивает нулями. Обе формы — редкие гости в прикладном коде, но для таблиц с пропусками (коды ошибок, номера клавиш) пригождаются. И важная договорённость на будущее: нули на пропусках — не ошибка, а правило языка; недостающие значения считаются заданными нулевым значением своего типа.
Четыре формы литерала
GOpackage 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 не написать — границей служит длина, вшитая в тип.
Перебор на практике
GOpackage 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».
Сравнение массивов через ==#
У массивов есть роскошь, которой срезы лишены: их можно сравнивать оператором ==. Сравнение поэлементное: два массива равны, если равны их длины (а они совпадают уже из типа) и каждый элемент первого равен соответствующему элементу второго. Условие 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 |
Каждый пример урока прогнан пробником на запасном TS-интерпретаторе — том, что работает и в bun-скриптах. Отличия от настоящего Go, все проверены: (1) var arr [3]int без литерала даёт массив нулевой длины вместо трёх нулей; (2) индексированный литерал [4]int{0: 100, 2: 200} печатается как [100 200] — объявленная длина игнорируется; (3) частичный литерал [4]int{1, 2} — то же самое; (4) fmt.Printf с %T печатает [N]interface {} вместо [3]int; (5) присваивание [3]int переменной типа [4]int не отклоняется, хотя настоящий Go его не компилирует. Основной движок песочницы — настоящий Go — от этих странностей свободен. Поэтому все ранимые примеры урока используют литералы, где число элементов совпадает с длиной: на таких массивах оба движка дают одинаковый результат.
Соберите программу: итог по размерам
Соберите программу: короткий литерал [...]int, range с подчёркиванием, аккумулятор до цикла и две итоговые строки — «итого: 82» и «элементов: 3».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: массивы
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 — оригинал останется целым: массив копируется целиком.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем массив отличается от среза?
Длина массива фиксирована и входит в тип, срез растёт и ужимается. Массив — значение: присваивание копирует все элементы; срез копирует маленький заголовок, указывая на те же данные. Массивы сравниваются через ==, для срезов сравнение запрещено. Срез — надстройка над массивом: он смотрит окном в непрерывный кусок памяти.
Почему [3]int и [4]int — разные типы?
Потому что длина — часть типа: компилятор по типу знает точный размер в памяти и проверяет каждую передачу массива между переменными, параметрами и возвратами. Присвоить [3]int переменной типа [4]int нельзя — программа просто не скомпилируется. Это цена и выгода фиксированной длины: меньше сюрпризов в рантайме, больше проверок на старте.
Зачем учить массивы, если в коде почти всегда срезы?
Три причины. Первая: ниши, где массив — правильный выбор, это хеши [32]byte, буферы фиксированного размера и сетки. Вторая: срезы устроены поверх массивов, и без понимания массивов-значений поведение срезов выглядит магией. Третья: range, индексация и границы у них общие — всё, что вы отработали на массивах здесь, переносится на срезы дословно.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Продвинутые возможности Go: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления