append: тот же массив или новый
В дне 16 было правило: результат append обязательно присваивают, s = append(s, x). Сегодня видно, откуда оно: append возвращает новый заголовок, а иногда и новый массив. Какой случай — решает ёмкость.
Есть место — append пишет в тот же массив
Ситуация. Взяли две первые ячейки и дописали третью.
stock := []int{12, 40, 7, 25, 3}
head := stock[:2]
head = append(head, 99)
fmt.Println(head, len(head), cap(head))
fmt.Println(stock)Что увидите.
[12 40 99] 3 5
[12 40 99 25 3]Как это читать. Перед append у head длина 2, ёмкость 5: за окном ещё три ячейки того же массива. append рассуждает просто: место есть — кладу элемент в следующую ячейку массива, длина на единицу больше.
до: head: len 2 cap 5
┌────┬────┬────┬────┬────┐
│ 12 │ 40 │ 7 │ 25 │ 3 │
└────┴────┴────┴────┴────┘
└─ head ─┘
после: head: len 3 cap 5
┌────┬────┬────┬────┬────┐
│ 12 │ 40 │ 99 │ 25 │ 3 │
└────┴────┴────┴────┴────┘
└──── head ────┘Следующая ячейка — stock[2]: в ней было 7, теперь 99. stock ни в каком append не участвовал, а элемент затёрт.
Места нет — append выделяет новый массив
Ситуация. Срез заполнен до конца массива, а к нему дописывают.
stock := []int{12, 40, 7}
more := append(stock, 5)
more[0] = 99
fmt.Println(stock[0], more[0])
fmt.Println(len(stock), cap(stock), len(more), cap(more))Что увидите.
12 99
3 3 4 6Как это читать. У stock длина 3 и ёмкость 3 — свободных ячеек нет. append заводит новый массив побольше, копирует туда три числа, дописывает 5 и возвращает заголовок на него. Дальше stock и more ячеек не делят: запись в more[0] до stock не доходит.
stock ──▶ ┌────┬────┬────┐
│ 12 │ 40 │ 7 │ старый массив
└────┴────┴────┘
more ──▶ ┌────┬────┬────┬────┬────┬────┐
│ 99 │ 40 │ 7 │ 5 │ │ │ новый массив
└────┴────┴────┴────┴────┴────┘Ёмкость нового массива наизусть не учат. Здесь Go 1.27.1 выбрал 6, но это не правило «удвоить». Вот как в той же версии растёт ёмкость среза, который собирают из пустого по элементу:
var s []int
for i := 1; i <= 5; i++ {
s = append(s, i)
fmt.Println(s, len(s), cap(s))
}[1] 1 1
[1 2] 2 2
[1 2 3] 3 4
[1 2 3 4] 4 4
[1 2 3 4 5] 5 8И даже эти числа — не правило языка. Уберите из печати сам срез, оставьте len и cap — и на первом элементе ёмкость окажется 4: раз срез из функции не отдают, компилятор вправе разложить память иначе. Рост ёмкости — деталь реализации: в другой версии Go числа будут свои, а расчёт на них сломается при обновлении. Ёмкость не угадывают, её печатают: cap(s).
Два продолжения одного среза — главный дефект дня
Ситуация. От базы из двух позиций нужны два варианта заказа: с подарком 1 и с подарком 2.
stock := []int{12, 40, 7, 25, 3}
base := stock[:2]
a := append(base, 1)
b := append(base, 2)
fmt.Println(a[2], b[2], stock[2])Что увидите.
2 2 2Как это читать. У base длина 2 и ёмкость 5. Первый append кладёт 1 в ячейку номер 2 и возвращает a. Второй получает тот же base — длина 2, место есть — и кладёт 2 в ту же ячейку. Окна a и b смотрят в один массив, поэтому оба видят последнюю запись. Заодно испорчен stock[2].
┌────┬────┬────┬────┬────┐
│ 12 │ 40 │ 2 │ 25 │ 3 │ сначала 1, потом 2
└────┴────┴────┴────┴────┘
└─── a ───────┘
└─── b ───────┘Хуже всего, что тот же код иногда работает. Нет у базы свободной ёмкости — каждый append выделяет свой массив, и варианты не мешают:
var base []int
base = append(base, 12, 40, 7)
a := append(base, 1)
b := append(base, 2)
fmt.Println(a, b)
fmt.Println(len(base), cap(base))[12 40 7 1] [12 40 7 2]
3 3Одна и та же запись то портит данные, то нет — по ёмкости, которой в коде не видно. Отсюда два правила:
- Результат
appendприсваивают тому же срезу:s = append(s, x), и старым заголовком больше не пользуются. - Нужны от одного среза два продолжения или надо дополнить подсрез, а исходный ещё в работе, — сначала своя копия (следующая страница).
Попробуйте сейчас: два append от одной базы.
Цель: увидеть, как два варианта затирают друг друга.
1. Наберите:
✎ Наберите в файлscratch/main.gopackage main import "fmt" func main() { stock := []int{12, 40, 7, 25, 3} base := stock[:2] fmt.Println(len(base), cap(base)) a := append(base, 1) b := append(base, 2) fmt.Println(a, b, stock) }2. Запустите:
▶ Выполнитеcd ~/gocourse/day17/scratch go run .3. Замените первую строку тела на
stock := []int{12, 40}и запустите ещё раз. Сравнитеcap(base)и то, что стало сaиb.Готово, когда: вы можете объяснить по ёмкости, почему в первом запуске
aиbодинаковые, а во втором разные.
Попробуйте сейчас: что выведет append в соседа.
Цель: предсказать вывод и сдать ответ.
1. Не запуская, нарисуйте массив и два окна —
firstиsecond:Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func main() { cells := []int{5, 9, 14, 2, 7} first := cells[:2] second := cells[2:] first = append(first, 40) fmt.Println(second[0], cells[2], len(first)) }2. Сдайте три числа через пробел:
▶ Выполните · выделенное замените своимcourse answer day17.q2 ЧИСЛО ЧИСЛО ЧИСЛО3. Проверьте запуском — наберите программу в черновик:
✎ Наберите в файлscratch/main.gopackage main import "fmt" func main() { cells := []int{5, 9, 14, 2, 7} first := cells[:2] second := cells[2:] first = append(first, 40) fmt.Println(second[0], cells[2], len(first)) }▶ Выполнитеcd ~/gocourse/day17/scratch go run .Готово, когда: пункт
q2вcourse check 17зелёный.
Попробуйте сейчас: что выведет append без ёмкости.
Цель: предсказать вывод, когда
appendвыделяет новый массив.1. Не запуская, сравните
lenиcapуcells:Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func main() { cells := []int{5, 9, 14} more := append(cells, 20) more[0] = 1 cells[1] = 2 fmt.Println(cells[0], more[1], len(more)) }2. Сдайте три числа через пробел:
▶ Выполните · выделенное замените своимcourse answer day17.q3 ЧИСЛО ЧИСЛО ЧИСЛО3. Проверьте запуском — наберите программу в черновик:
✎ Наберите в файлscratch/main.gopackage main import "fmt" func main() { cells := []int{5, 9, 14} more := append(cells, 20) more[0] = 1 cells[1] = 2 fmt.Println(cells[0], more[1], len(more)) }▶ Выполнитеcd ~/gocourse/day17/scratch go run .Готово, когда: пункт
q3вcourse check 17зелёный.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
append(stock, 5) (value of type []int) is not used |
результат append не присвоен |
stock = append(stock, 5) |
invalid append: argument must be a slice; have total (variable of type int) |
первым аргументом append стоит не срез |
первым — срез, дальше — элементы |
элемент одного среза «сам» поменялся после append в другой |
у второго была свободная ёмкость, append писал в общий массив |
для второго продолжения — своя копия |
| данные портятся на одних вводах и не портятся на других | всё решает ёмкость, а она зависит от того, как срез собирали | не рассчитывать на ёмкость, делать копию |
Дальше: своя копия, nil-срез и пустой срез — course next