append и make
Литерал []int{1, 2, 3} подходит, когда элементы известны при написании кода. Чётные числа из ввода или найденные слова собирают по одному во время работы программы. Окно на шкафчики тогда должно расти.
Новый элемент в конец среза добавляет встроенная функция append:
package main
import "fmt"
func main() {
var s []int
fmt.Println(len(s), s == nil) // 0 true
s = append(s, 1)
s = append(s, 2, 3)
fmt.Println(s, len(s)) // [1 2 3] 3
}Давайте разберём пример по строкам:
var s []intобъявляет срез без элементов, поэтому первыйPrintlnпечатает0 true.s = append(s, 1)возвращает срез[1], и мы присваиваем его обратно вs.s = append(s, 2, 3)дописывает в конец сразу два значения.- Последний
Printlnпечатает[1 2 3] 3.
Нулевой срез nil
Нулевое значение среза — nil: окно, которое ещё ни на какой ряд не смотрит. У такого среза три свойства:
- в нём нет элементов, и
lenвозвращает0; fmt.Printlnпечатает его как[];- с ним работает
append, поэтому для сбора значений хватает объявленияvar s []int.
Проверить, пустое ли окно, можно сравнением s == nil. Это единственное сравнение, которое Go разрешает для среза.
Вызов append
Вызов append(s, v) возвращает срез, в котором после последнего элемента s стоит v. Разберём его части:
- первый аргумент — срез, к которому добавляют;
- остальные аргументы — новые элементы, и за один вызов их может быть несколько:
append(s, 2, 3); - результат — срез, который присваивают обратно в ту же переменную.
Срез нужной длины
Когда длина известна заранее, срез создают встроенной функцией make. У неё две формы.
Длина: make([]int, 3)
Вызов make([]int, 3) создаёт срез из трёх нулей:
[]int— тип среза;3— длина, число элементов.
Элементы сразу можно менять по индексу. Эти нули — обычные элементы: append допишет новое значение после них, а не на место первого нуля.
Длина и ёмкость: make([]int, 0, 5)
Третий аргумент задаёт ёмкость — сколько элементов поместится в срез всего, считая уже созданные. В нашей картинке это число шкафчиков от левого края окна до конца ряда. Вызов make([]int, 0, 5) создаёт пустой срез на пять элементов, а cap(b) возвращает это число.
Обе формы рядом:
a := make([]int, 3)
a[1] = 7
fmt.Println(a) // [0 7 0]
b := make([]int, 0, 5)
fmt.Println(len(b), cap(b)) // 0 5- В
aтри нуля, иa[1] = 7меняет средний, поэтому программа печатает[0 7 0]. - У
bв окне не видно ни одного шкафчика, но в ряду их пять: программа печатает0 5.
Устройство среза
Срез хранит три значения, и вместе их называют заголовком среза:
- ссылку на массив, где лежат элементы, — этот массив называют базовым массивом;
- длину;
- ёмкость — число элементов базового массива, считая от начала среза.
Что делает append, зависит от того, осталось ли место.
Ёмкости хватает
Пока длина меньше ёмкости, append пишет новый элемент в тот же массив, и окно расширяется на один шкафчик. Копирования нет.
Ёмкости не хватает
Когда места нет, append действует в три шага:
- Создаёт массив побольше.
- Копирует туда старые элементы.
- Возвращает срез, который указывает на новый массив.
Новый массив берётся с запасом, чтобы следующие вызовы append снова писали без копирования. Насколько он больше, решает Go, и точный множитель зависит от версии и от размера среза.
Нажмите кнопку и посмотрите, когда append пишет в тот же массив, а когда переносит элементы в новый.
Срез: длина 2, ёмкость 2 — запаса нет. Нажмите на кнопку.
Здесь аналогия со шкафчиками заканчивается: настоящие вещи никто не переносит в новый ряд сам. А append переносит, и старое окно продолжает смотреть на старый ряд.
У среза из make([]int, 0, 5) ёмкость 5: первые пять вызовов append пишут в один массив, а шестой создаёт новый. Переменная, которой результат не присвоили, по-прежнему хранит старую длину.
Этот вопрос часто задают на собеседовании: «Что такое срез и как append его наращивает?» Ответ собирается из трёх частей:
- срез — это заголовок из ссылки на базовый массив, длины и ёмкости;
- пока есть свободная ёмкость,
appendпишет на месте, а когда её нет, создаёт массив побольше и копирует туда элементы; - заголовок после вызова может быть другим, поэтому результат всегда присваивают:
s = append(s, x).
Частая ошибка
Результат append не присвоили:
s := []int{1, 2, 3}
append(s, 4) // ✗append(s, 4) (value of type []int) is not usedКомпилятор не пропускает такой вызов: append не меняет переменную s, а только возвращает новый срез. Пишите s = append(s, 4).
Главное
appendвозвращает новый срез, и результат присваивают:s = append(s, v).make([]int, n)создаётnнулевых элементов, аmake([]int, 0, c)— пустой срез ёмкостиc.- Срез — заголовок из ссылки на базовый массив, длины и ёмкости.
- Когда ёмкости не хватает,
appendсоздаёт массив побольше и копирует туда элементы.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.