День 17 — глубже
Всё здесь необязательно. Материалы на английском помечены.
Как устроен срез — от авторов Go (англ.)
- Go Slices: usage and internals — https://go.dev/blog/slices-intro Статья с теми же картинками, что на странице 1: массив, заголовок из указателя, длины и ёмкости, подсрез как новое окно.
- Arrays, slices (and strings): The mechanics of 'append' — https://go.dev/blog/slices Подробнее про
append: почему функция возвращает срез и почему заголовок, переданный в функцию, — копия. Раздел про строки пригодится к дню 19.
- Пакет slices — https://pkg.go.dev/slices Справочник по всем функциям пакета. Сегодня нужны
Sort,Contains,Index,Max,Min,Equal,Clone; остальные — по мере надобности.
Типовые ошибки со срезами (англ.)
- 100 Go Mistakes and How to Avoid Them, глава про срезы — https://100go.co/#slices Ошибки 20–26:
lenиcap, срез от nil,appendв общий массив, неполная копия черезcopy. Каждая — с короткой программой; все их теперь можно прочитать по картинкам этого дня.
Третье число у 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 {cases— срез:[]перед типом значит то же, что в[]int;- каждая строка в фигурных скобках — один элемент среза;
for _, c := range cases— обход среза из дня 16,c— текущий элемент.
Что такое struct { … } — тип элемента из нескольких полей — разберём в блоке 5.
Таблица трассировки отладочной печатью
Таблицу со страницы 7 можно получить из самой программы: после каждой строки печатать срезы, их длины и ёмкости. Приём годится, чтобы сверить спорный шаг, — но только после того, как таблица заполнена своей головой.
Попробуйте сейчас: сверить таблицу отладочной печатью.
Цель: увидеть каждую строку таблицы со страницы 7 в выводе программы.
1. Наберите копию программы с печатью после каждой строки:
✎ Наберите в файлscratch/main.gopackage 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 2head[:4] законен: четыре ячейки помещаются в ёмкость 5. А head[3] — паника: индекс проверяется по длине 2. Ячейки за окном никуда не деваются, и через подсрез их можно снова увидеть. Это ещё одна причина, почему чувствительные данные не прячут, просто укоротив срез.
Подсрез с тремя числами: s[a:b:c]
Третье число в подсрезе ограничивает ёмкость: stock[:2:2] — окно на две ячейки и ёмкость 2. Следующий append вынужден выделить новый массив, и два продолжения одной базы перестают мешать друг другу.
Попробуйте сейчас: база без свободной ёмкости.
Цель: сравнить два
appendотstock[:2]и отstock[:2:2].✎ Наберите в файлscratch/main.gopackage 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: копия — там, где один код меняет данные, а другой ещё их читает.