День 16 · Срезы: много значений в одной переменной · страница 1 из 7

Много значений: массив и срез

До сих пор у каждого значения была своя переменная: qty, price, total. Сегодня значений будет столько, сколько пришло во вводе, — и заранее это число неизвестно. Для этого в Go есть срез.


Двадцать ячеек — двадцать переменных?

Ситуация. На стеллаже двадцать ячеек, в каждой свой остаток. Нужно сложить остатки, найти самую полную ячейку и перечислить те, где товара мало. Со знаниями блоков 1–3 это выглядит так:

Код для чтения · разбираем, набирать не нужно
	cell1 := 12
	cell2 := 40
	cell3 := 5
	// ... ещё семнадцать строк
	total := cell1 + cell2 + cell3 // + ещё семнадцать слагаемых

Беда не в длине: цикл из дня 08 не умеет перебирать имена cell1, cell2, cell3, а завтра стеллаж станет на тридцать ячеек. Нужна одна переменная с рядом значений и способ сказать «элемент номер такой-то».


Массив: ряд фиксированной длины

Дальше показываем только строки из тела main — вставляйте их между { и } в scratch/main.go. С одним import "fmt" первая строка тела — шестая.

Код для чтения · разбираем, набирать не нужно
	var cells [5]int
	cells[0] = 12
	fmt.Println(cells, len(cells))

Что увидите.

Вывод · это печатает программа, набирать не нужно
[12 0 0 0 0] 5

Как это читать.

Запись Что значит
[5]int тип «ряд из пяти целых». Это массив
var cells [5]int пять ячеек сразу, в каждой нулевое значение 0 — как у обычной var qty int
cells[0] = 12 записать 12 в элемент с индексом 0
len(cells) длина — сколько элементов в ряду
[12 0 0 0 0] так Println печатает ряд: элементы через пробел в квадратных скобках

Главное свойство массива: длина — часть типа. [5]int и [6]int — разные типы, как int и string; длину нельзя взять из переменной и нельзя поменять. Поэтому дальше в курсе вместо массива почти всегда срез.


Срез: ряд, длина которого не записана в типе

Код для чтения · разбираем, набирать не нужно
	stock := []int{12, 40, 5, 40, 7}
	fmt.Println(stock[0], stock[2])
	stock[2] = 15
	fmt.Println(stock, len(stock))

Что увидите.

Вывод · это печатает программа, набирать не нужно
12 5
[12 40 15 40 7] 5

Как это читать. []int — квадратные скобки пустые: это срез целых, длина в тип не входит. В фигурных скобках — начальные элементы. Картинка того, что получилось в памяти:

Образец · набирать не нужно
               индекс:   0    1    2    3    4
                       ┌────┬────┬────┬────┬────┐
  stock ─────────────► │ 12 │ 40 │ 15 │ 40 │  7 │
                       └────┴────┴────┴────┴────┘
  len(stock) = 5

Сам ряд ячеек Go заводит за вас, а переменная stock — окно на него: видно, где ряд начинается и сколько в нём элементов. Как устроено окно — день 17. Сегодня достаточно трёх правил:

Номер ячейки на складе и индекс в срезе — разные вещи: ячейка 1 лежит в stock[0]. Эта единица разницы — источник половины сегодняшних ошибок.


Попробуйте сейчас: первый срез в черновике.

Цель: программа печатает срез, его длину и изменённый элемент.

1. Наберите в черновик:

✎ Наберите в файл scratch/main.go
package main

import "fmt"

func main() {
	stock := []int{12, 40, 5, 40, 7}
	fmt.Println(stock[0], stock[2])
	stock[2] = 15
	fmt.Println(stock, len(stock))
}

2. Запустите:

▶ Выполните
cd ~/gocourse/day16/scratch
go run .

3. Напечатайте последний элемент, не считая его индекс в уме: допишите строку fmt.Println(stock[len(stock)-1]) и запустите снова.

Готово, когда: вывод совпал с разделом «Что увидите» выше, а третья строка — 7.


Попробуйте сейчас: что выведет запись по индексу.

Цель: предсказать три числа, не запуская программу, и сдать ответ.

1. Не запуская, прочитайте строки тела main:

Код для чтения · разбираем, набирать не нужно
	stock := []int{12, 40, 5, 40}
	i := 2
	stock[i] = stock[i-1] + 1
	fmt.Println(stock[2], stock[i+1], len(stock))

Индекс может быть не только числом, но и переменной или выражением — stock[i-1] значит «элемент, индекс которого на единицу меньше i».

2. Сдайте три числа через пробел, в том порядке, в каком их напечатает программа:

▶ Выполните · выделенное замените своим
course answer day16.q1 ЧИСЛО ЧИСЛО ЧИСЛО

3. Проверьте запуском — замените тело main в черновике:

✎ Наберите в файл scratch/main.go
package main

import "fmt"

func main() {
	stock := []int{12, 40, 5, 40}
	i := 2
	stock[i] = stock[i-1] + 1
	fmt.Println(stock[2], stock[i+1], len(stock))
}
▶ Выполните
go run .

Готово, когда: пункт q1 в course check 16 зелёный.


Что может пойти не так

У массива длина известна компилятору, поэтому выход за границу с индексом-числом он ловит сам. Строки тела main:

Код для чтения · разбираем, набирать не нужно
	var cells [5]int
	cells[5] = 3
	fmt.Println(cells)
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day16/scratch$ go run .
# day16/scratch
./main.go:7:8: invalid argument: index 5 out of bounds [0:5]

У среза длина станет известна только при запуске. Та же ошибка в срезе — здесь чтение stock[5] — компилируется молча и роняет программу уже во время работы:

Код для чтения · разбираем, набирать не нужно
	stock := []int{12, 40, 5, 40, 7}
	fmt.Println(stock[5])
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day16/scratch$ go run .
panic: runtime error: index out of range [5] with length 5

goroutine 1 [running]:
main.main()
	/home/stagiaire/gocourse/day16/scratch/main.go:7 +0x9
exit status 2

Что такое panic и как её читать — на третьей странице.

Что видите Что это значит Что делать
invalid argument: index 5 out of bounds [0:5] у массива из пяти элементов индексы 0–4, а написан 5 последний индекс — длина минус один
invalid argument: index -1 (constant of type int) must not be negative индекс меньше нуля индексы начинаются с 0
panic: runtime error: index out of range [5] with length 5 в срезе пять элементов, а спросили индекс 5 то же самое, но ловится только при запуске
cannot use cells (variable of type [3]int) as []int value in argument to total передали массив туда, где ждут срез: это разные типы объявите cells := []int{…} — без числа в скобках
две строки: declared and not used: size и invalid array length size длина массива взята из переменной, а она должна быть известна до запуска нужен срез, а не массив

Попробуйте сейчас: репозиторий дня.

Цель: в ~/gocourse/day16 есть репозиторий с первым коммитом — заготовками дня. Каждое задание дальше заканчивается своим коммитом.

▶ Выполните
cd ~/gocourse/day16
git init
git add .
git commit -m "день 16: заготовки"
git log --oneline

Готово, когда: git log --oneline печатает одну строку с вашим сообщением. Подпись автора задана ещё в день 11; если git просит её — git config --global user.name и user.email, как тогда.


Дальше: как срез растёт — append — course next