День 17 · дополнительные материалы

День 17 — глубже

Всё здесь необязательно. Материалы на английском помечены.


Как устроен срез — от авторов Go (англ.)

Типовые ошибки со срезами (англ.)

Третье число у make: длина 0, место на N

make([]int, 0, 3) даёт срез длины 0 с местом на три элемента. Для copy он не годится — поместится ноль элементов, — зато первые три append обходятся без нового массива:

Код для чтения · разбираем, набирать не нужно
a := make([]int, 3)
b := make([]int, 0, 3)
fmt.Println(a, len(a), cap(a))
fmt.Println(b, len(b), cap(b))
b = append(b, 5, 9)
fmt.Println(b, len(b), cap(b))
Вывод · это печатает программа, набирать не нужно
[0 0 0] 3 3
[] 0 3
[5 9] 2 3

Так пишут, когда заранее известно, сколько элементов соберётся: ёмкость задана сразу, и append не переселяет массив по дороге. Правильности это не меняет — только работы машине меньше.

Сортировка строк: по буквам и по байтам

slices.Sort и slices.Index работают не только с числами:

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

import (
	"fmt"
	"slices"
)

func main() {
	names := []string{"шпилька", "болт", "анкер", "гайка"}
	slices.Sort(names)
	fmt.Println(names)
	fmt.Println(slices.Index(names, "гайка"))
	mixed := []string{"болт", "Болт", "ёлка", "яблоко", "анкер"}
	slices.Sort(mixed)
	fmt.Println(mixed)
}
Вывод · это печатает программа, набирать не нужно
[анкер болт гайка шпилька]
2
[Болт анкер болт яблоко ёлка]

Слова из маленьких русских букв встают по алфавиту, а во второй строке порядок неожиданный: заглавная «Б» впереди всех, «ё» — после «я». Строки сравниваются по байтам, а не по алфавиту; из чего состоит строка, разберём в дне 19.

Как устроена таблица случаев в тесте

Форму таблицы в дне 14 набирали по образцу. Теперь половину её устройства видно насквозь:

Код для чтения · разбираем, набирать не нужно
cases := []struct {
	name string
	qty  int
	want int
}{
	{"обычный заказ", 3, 13500},
	{"ноль", 0, 0},
}
for _, c := range cases {

Что такое struct { … } — тип элемента из нескольких полей — разберём в блоке 5.

Таблица трассировки отладочной печатью

Таблицу со страницы 7 можно получить из самой программы: после каждой строки печатать срезы, их длины и ёмкости. Приём годится, чтобы сверить спорный шаг, — но только после того, как таблица заполнена своей головой.

Попробуйте сейчас: сверить таблицу отладочной печатью.

Цель: увидеть каждую строку таблицы со страницы 7 в выводе программы.

1. Наберите копию программы с печатью после каждой строки:

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

import "fmt"

func main() {
	stock := []int{7, 4, 9, 2}
	fmt.Println("после 6:", stock, len(stock), cap(stock))
	head := stock[:2]
	fmt.Println("после 7:", stock, head, len(head), cap(head))
	head = append(head, 5)
	fmt.Println("после 8:", stock, head, len(head), cap(head))
	head = append(head, 8)
	fmt.Println("после 9:", stock, head, len(head), cap(head))
	head = append(head, 1)
	fmt.Println("после 10:", stock, head, len(head), cap(head))
	head[0] = 6
	fmt.Println("после 11:", stock, head, len(head), cap(head))
}

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

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

Готово, когда: каждая строка вывода совпала со строкой таблицы с тем же номером, а len(stock) и cap(stock) вы сверили по шагу 1 — они с тех пор не менялись.

Подсрез дальше длины — до ёмкости

Индекс s[i] ограничен длиной, а конец подсреза — ёмкостью. Поэтому из окна в две ячейки можно взять окно пошире:

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

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

head[:4] законен: четыре ячейки помещаются в ёмкость 5. А head[3] — паника: индекс проверяется по длине 2. Ячейки за окном никуда не деваются, и через подсрез их можно снова увидеть. Это ещё одна причина, почему чувствительные данные не прячут, просто укоротив срез.

Подсрез с тремя числами: s[a:b:c]

Третье число в подсрезе ограничивает ёмкость: stock[:2:2] — окно на две ячейки и ёмкость 2. Следующий append вынужден выделить новый массив, и два продолжения одной базы перестают мешать друг другу.

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

Цель: сравнить два append от stock[:2] и от stock[:2:2].

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

import "fmt"

func main() {
	stock := []int{12, 40, 7, 25, 3}
	base := stock[:2:2]
	a := append(base, 1)
	b := append(base, 2)
	fmt.Println(a, b, stock)
	fmt.Println(len(base), cap(base))
}
▶ Выполните
cd ~/gocourse/day17/scratch
go run .

Готово, когда: a и b разные, stock не изменился, а замена stock[:2:2] на stock[:2] возвращает дефект второй страницы.

Такую запись встретите в чужом коде как защиту от дефекта «два среза на один массив». В своём коде на курсе понятнее явная копия slices.Clone: её видно сразу, а третье число в скобках легко не заметить.

Сколько весит копия

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