Перейти к содержимому
Теория · Массивы и срезы · урок 4 из 10

append и make

Литерал []int{1, 2, 3} подходит, когда элементы известны при написании кода. Чётные числа из ввода или найденные слова собирают по одному во время работы программы. Окно на шкафчики тогда должно расти.

Новый элемент в конец среза добавляет встроенная функция append:

GO
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
}

Давайте разберём пример по строкам:

  1. var s []int объявляет срез без элементов, поэтому первый Println печатает 0 true.
  2. s = append(s, 1) возвращает срез [1], и мы присваиваем его обратно в s.
  3. s = append(s, 2, 3) дописывает в конец сразу два значения.
  4. Последний 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) возвращает это число.

Обе формы рядом:

GO
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
  1. В a три нуля, и a[1] = 7 меняет средний, поэтому программа печатает [0 7 0].
  2. У b в окне не видно ни одного шкафчика, но в ряду их пять: программа печатает 0 5.

Устройство среза

Срез хранит три значения, и вместе их называют заголовком среза:

  • ссылку на массив, где лежат элементы, — этот массив называют базовым массивом;
  • длину;
  • ёмкость — число элементов базового массива, считая от начала среза.

Что делает append, зависит от того, осталось ли место.

Ёмкости хватает

Пока длина меньше ёмкости, append пишет новый элемент в тот же массив, и окно расширяется на один шкафчик. Копирования нет.

Ёмкости не хватает

Когда места нет, append действует в три шага:

  1. Создаёт массив побольше.
  2. Копирует туда старые элементы.
  3. Возвращает срез, который указывает на новый массив.

Новый массив берётся с запасом, чтобы следующие вызовы append снова писали без копирования. Насколько он больше, решает Go, и точный множитель зависит от версии и от размера среза.

Нажмите кнопку и посмотрите, когда append пишет в тот же массив, а когда переносит элементы в новый.

ВживуюСрез: appendlen = 2 · cap = 2 · массив 1
← массив заполнен до конца

Срез: длина 2, ёмкость 2 — запаса нет. Нажмите на кнопку.

Следите за номером массива

Здесь аналогия со шкафчиками заканчивается: настоящие вещи никто не переносит в новый ряд сам. А append переносит, и старое окно продолжает смотреть на старый ряд.

У среза из make([]int, 0, 5) ёмкость 5: первые пять вызовов append пишут в один массив, а шестой создаёт новый. Переменная, которой результат не присвоили, по-прежнему хранит старую длину.

Этот вопрос часто задают на собеседовании: «Что такое срез и как append его наращивает?» Ответ собирается из трёх частей:

  • срез — это заголовок из ссылки на базовый массив, длины и ёмкости;
  • пока есть свободная ёмкость, append пишет на месте, а когда её нет, создаёт массив побольше и копирует туда элементы;
  • заголовок после вызова может быть другим, поэтому результат всегда присваивают: s = append(s, x).

Частая ошибка

Результат append не присвоили:

GO
s := []int{1, 2, 3}
append(s, 4) // ✗
TEXT
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 и подпишись на канал — откроются все уроки.

Войти другим способом