← Назад к вопросам

Почему append возвращает slice?

2.2 Middle🔥 131 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему функция append возвращает новый срез

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

1. Устройство среза и capacity

Срез в Go — это дескриптор, состоящий из трех компонентов:

  • Указатель на базовый массив
  • Длина (length) — текущее количество элементов
  • Ёмкость (capacity) — максимальное количество элементов без переаллокации
// Структура среза (логическое представление)
type sliceHeader struct {
    Pointer *byte
    Length  int
    Capacity int
}

Когда мы добавляем элементы с помощью append, может произойти два сценария:

Сценарий 1: Вместимости достаточно

Если в базовом массиве есть свободное место (длина < ёмкость), append добавляет элементы в существующий массив:

func main() {
    s := make([]int, 2, 4) // Длина 2, ёмкость 4
    s[0], s[1] = 1, 2
    
    s = append(s, 3) // Добавляем 3
    // s теперь указывает на тот же массив, но длина стала 3
}

Сценарий 2: Требуется переаллокация

Если ёмкости недостаточно, Go создает новый массив с большей ёмкостью (обычно в 2 раза больше для небольших срезов) и копирует в него старые элементы:

func main() {
    s := []int{1, 2} // Длина 2, ёмкость 2
    
    s = append(s, 3) // Требуется переаллокация!
    // Создается новый массив, s теперь указывает на него
}

2. Причины возврата нового среза

Безопасность и предсказуемость

Возврат нового среза делает поведение функции предсказуемым. Разработчик всегда знает, что должен использовать возвращаемое значение:

func main() {
    s1 := []int{1, 2, 3}
    s2 := append(s1, 4) // s1 не изменяется!
    
    fmt.Println(s1) // [1 2 3]
    fmt.Println(s2) // [1 2 3 4]
}

Избегание побочных эффектов

Если бы append изменял исходный срез, это могло бы привести к неожиданным побочным эффектам:

func processSlice(s []int) {
    // Если бы append изменял s, это повлияло бы на оригинал
    s = append(s, 100)
}

func main() {
    original := []int{1, 2, 3}
    processSlice(original)
    // original остался [1, 2, 3], что безопасно
}

Совместимость с capacity изменениями

Поскольку при переаллокации создается новый базовый массив, срез должен получить новый дескриптор с обновленным указателем. Возврат значения — единственный способ сообщить об этом изменении.

3. Особенности работы с capacity

func demonstrateAppendBehavior() {
    // Пример 1: Без переаллокации
    s1 := make([]int, 3, 5)
    s1[0], s1[1], s1[2] = 1, 2, 3
    
    s2 := append(s1, 4)
    // s1 и s2 разделяют базовый массив!
    s2[0] = 99
    fmt.Println(s1[0]) // 99 - изменение в s2 повлияло на s1
    
    // Пример 2: С переаллокацией
    s3 := []int{1, 2, 3} // capacity = 3
    s4 := append(s3, 4)  // Требуется переаллокация
    s4[0] = 100
    fmt.Println(s3[0]) // 1 - s3 не изменился, разные массивы
}

4. Практические следствия

Всегда присваивайте результат append

// Правильно:
slice = append(slice, element)

// Ошибка (результат теряется):
append(slice, element)

Работа с несколькими добавлениями

// Эффективно - одна потенциальная переаллокация
slice = append(slice, a, b, c)

// Менее эффективно - возможны multiple переаллокации
slice = append(slice, a)
slice = append(slice, b)
slice = append(slice, c)

Идиоматический паттерн для построения срезов

func collectValues() []string {
    var result []string // nil-срез
    
    for i := 0; i < 10; i++ {
        result = append(result, fmt.Sprintf("item-%d", i))
    }
    
    return result
}

5. Сравнение с другими языками

В отличие от:

  • Python (list.append() модифицирует список на месте)
  • Java (ArrayList.add() модифицирует список)
  • JavaScript (Array.push() модифицирует массив)

Go выбрал подход с возвратом значения для:

  1. Явности — изменение состояния всегда видно в коде
  2. Контроля над аллокациями — разработчик решает, когда переиспользовать срез
  3. Потокобезопасности — уменьшает гонки данных при правильном использовании

Заключение

Возврат нового среза функцией append — это сознательное дизайнерское решение Go, которое обеспечивает:

  • Прозрачность поведения при переаллокациях
  • Безопасность от неожиданных изменений
  • Гибкость в управлении памятью
  • Согласованность с философией Go "явное лучше неявного"

Этот подход требует от разработчика понимания устройства срезов, но в результате дает более предсказуемый и контролируемый код, особенно важный в системном программировании, для которого создавался Go.

Почему append возвращает slice? | PrepBro