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

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

Одна и та же запись то портит данные, то нет — по ёмкости, которой в коде не видно. Отсюда два правила:

  1. Результат append присваивают тому же срезу: s = append(s, x), и старым заголовком больше не пользуются.
  2. Нужны от одного среза два продолжения или надо дополнить подсрез, а исходный ещё в работе, — сначала своя копия (следующая страница).

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

Цель: увидеть, как два варианта затирают друг друга.

1. Наберите:

✎ Наберите в файл scratch/main.go
package 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.go
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))
}
▶ Выполните
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.go
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))
}
▶ Выполните
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