Как работает append?go-75

Базовое поведение функции append

Функция append в Go используется для добавления элементов в срезы (slices). Её сигнатура:

func append(slice []Type, elems ...Type) []Type

Простой пример использования:

nums := []int{1, 2, 3}
nums = append(nums, 4) // [1, 2, 3, 4]

Ключевые аспекты работы append

1. Механизм расширения capacity

Когда длина (length) превышает ёмкость (capacity), append:

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

Правило роста capacity (до Go 1.18):

  • Новый capacity = max(удвоенный старый capacity, требуемый capacity)

С Go 1.18 алгоритм стал сложнее:

newcap := oldcap
doublecap := newcap + newcap
if newLen > doublecap {
    newcap = newLen
} else {
    const threshold = 256
    if oldcap < threshold {
        newcap = doublecap
    } else {
        for newcap < newLen {
            newcap += (newcap + 3*threshold) / 4
        }
    }
}

2. Поведение с разными capacity

Пример с разными сценариями:

a := make([]int, 2, 4) // length=2, capacity=4
b := append(a, 1)      // Использует существующий буфер
c := append(a, 1, 2, 3) // Требуется переаллокация

3. Особенности работы с памятью

  • Возвращаемый срез может указывать на новый массив
  • Исходный срез не изменяется (если не было переаллокации)
s1 := []int{1, 2}
s2 := append(s1, 3)
s1[0] = 9
// s1: [9, 2], s2: [1, 2, 3] или [9, 2, 3] в зависимости от capacity

Продвинутые сценарии использования

1. Добавление одного среза в другой

Используется оператор ...:

a := []int{1, 2}
b := []int{3, 4}
a = append(a, b...) // [1, 2, 3, 4]

2. Оптимизация производительности

Предварительное выделение capacity:

// Плохо: множественные переаллокации
var s []int
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

// Хорошо: предварительное выделение
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

3. Работа с nil-срезами

var s []int
s = append(s, 1) // Работает корректно

Подводные камни

  1. Неожиданное разделение памяти:
a := []int{1, 2, 3}
b := append(a[:2], 4)
// Изменение b может повлиять на a
  1. Гонки данных при параллельном использовании:

    • Append не является атомарной операцией
    • Требуется синхронизация при работе из нескольких горутин
  2. Утечки памяти:

    • Удержание ссылок на старые массивы после append

Производительность append

  1. Амортизированная сложность O(1)
  2. Критические случаи:
    • Частые переаллокации при неправильном выборе capacity
    • Копирование больших массивов

Резюмируем

append в Go — это мощный механизм работы со срезами, который автоматически управляет выделением памяти, но требует понимания его внутренней работы для эффективного использования и избежания распространённых ошибок, связанных с аллокацией и разделением памяти.