День 17 · Срезы изнутри: len, cap и общий массив · страница 1 из 7

Срез — окно на массив

В дне 16 срез вёл себя как список: добавили, прочитали по индексу, обошли циклом. Один факт остался без объяснения: функция меняет элементы среза, и это видно снаружи, хотя параметр int в дне 11 так не умел. Сегодня заглядываем внутрь среза.

День про ошибки, которых не видит компилятор: программа собирается, работает и тихо портит данные — один срез «сам» меняется, когда пишут в другой. Понять такое можно только по картинке памяти, поэтому картинок сегодня много.


Заголовок и массив

Ситуация. Остатки пяти ячеек:

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

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

Что увидите.

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

Как это читать. Числа лежат не в самой переменной stock. Литерал []int{…} делает две вещи: заводит массив из пяти ячеек — того вида [5]int, что был в дне 16, — и срез, который на него смотрит. Сам срез — три числа, их называют заголовком:

Образец · набирать не нужно
 stock:  указатель ─┐   len 5   cap 5
                    │
                    ▼
 массив:  ┌────┬────┬────┬────┬────┐
          │ 12 │ 40 │  7 │ 25 │  3 │
          └────┴────┴────┴────┴────┘
            0    1    2    3    4

Срез — это окно: указатель — где оно начинается, длина — ширина, ёмкость — сколько места до конца массива. Индексы считаются от начала окна, и видно только то, что внутри длины.


Подсрез: окно поуже

Ситуация. Нужны не все ячейки, а часть: со второй по третью.

Код для чтения · разбираем, набирать не нужно
stock := []int{12, 40, 7, 25, 3}
mid := stock[1:3]
fmt.Println(mid, len(mid), cap(mid))
head := stock[:2]
tail := stock[3:]
fmt.Println(head, len(head), cap(head))
fmt.Println(tail, len(tail), cap(tail))

Что увидите.

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

Как это читать. stock[1:3] — подсрез: окно с ячейки 1 до ячейки 3, не включая её. Новых ячеек не появляется: mid — ещё один заголовок на тот же массив.

Образец · набирать не нужно
 stock:  указатель ─┐                 len 5  cap 5
 mid:    указатель ─┼────┐            len 2  cap 4
                    ▼    ▼
 массив:  ┌────┬────┬────┬────┬────┐
          │ 12 │ 40 │  7 │ 25 │  3 │
          └────┴────┴────┴────┴────┘
            0    1    2    3    4
                 └─ mid ─┘
Запись Окно len cap у stock из пяти
stock[a:b] ячейки с a по b-1 b - a 5 - a
stock[:b] с начала, то же что stock[0:b] b 5
stock[a:] до конца, то же что stock[a:len(stock)] 5 - a 5 - a

Ёмкость подсреза считается от его начала до конца массива: mid начинается с ячейки 1, до конца массива четыре ячейки — cap 4. Ячейки за окном mid никуда не делись, просто через него их не видно.


Запись через подсрез

Ячейки общие — значит, запись через подсрез меняет исходный срез.

Код для чтения · разбираем, набирать не нужно
stock := []int{12, 40, 7, 25, 3}
mid := stock[1:3]
mid[0] = 99
fmt.Println(stock)
fmt.Println(mid)
Вывод · это печатает программа, набирать не нужно
[12 99 7 25 3]
[99 7]

mid[0] и stock[1] — одна и та же ячейка массива, у неё два адреса. Подсрез ничего не копирует: это способ работать с частью большого среза даром. Ошибкой это становится, когда автор кода думал, что подсрез — отдельная копия.


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

Цель: предсказать len и cap трёх подсрезов и сверить запуском.

1. Наберите программу и до запуска запишите на бумаге, что напечатает каждая строка:

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

import "fmt"

func main() {
	cells := []int{8, 30, 14, 2, 55, 6}
	a := cells[2:5]
	b := cells[:4]
	c := cells[4:]
	fmt.Println(a, len(a), cap(a))
	fmt.Println(b, len(b), cap(b))
	fmt.Println(c, len(c), cap(c))
}

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

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

Готово, когда: все девять чисел совпали с записанными, а если нет — вы нашли по картинке, где ошиблись.


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

Цель: предсказать вывод программы и сдать ответ.

1. Не запуская, нарисуйте массив и окно low:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	stock := []int{30, 12, 45, 8, 20}
	low := stock[1:4]
	low[0] = 0
	low[2] = low[2] + 5
	fmt.Println(stock[1], stock[3], len(low))
}

2. Сдайте три числа через пробел — как их напечатает Println:

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

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

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

import "fmt"

func main() {
	stock := []int{30, 12, 45, 8, 20}
	low := stock[1:4]
	low[0] = 0
	low[2] = low[2] + 5
	fmt.Println(stock[1], stock[3], len(low))
}
▶ Выполните
cd ~/gocourse/day17/scratch
go run .

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


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

Конец подсреза дальше конца массива:

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

goroutine 1 [running]:
main.main()
	/home/stagiaire/gocourse/day17/scratch/main.go:8 +0x51
exit status 2

Та же паника и с числом прямо в коде — stock[:7]: ёмкость компилятор не проверяет, её узнают при запуске.

Что видите Что это значит Что делать
panic: runtime error: slice bounds out of range [:7] with capacity 5 конец подсреза дальше конца массива: у stock пять ячеек ограничить конец: if n > len(stock) { n = len(stock) }
panic: runtime error: slice bounds out of range [3:1] начало подсреза больше конца, числа в переменных проверить порядок: сначала меньшее, s[1:3]
invalid slice indices: 1 < 3 то же самое, но оба числа прямо в коде — компилятор видит сам поменять местами
panic: runtime error: index out of range [3] with length 2 индекс считается от длины окна, а не от ёмкости индекс меньше len, а не cap

Дальше: append — когда он пишет в тот же массив, а когда в новый — course next