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

Есть ли динамический массив в Go?

1.0 Junior🔥 111 комментариев
#Основы Go

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

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

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

Динамические массивы в Go: slice vs array

В Go нет классических динамических массивов в том смысле, как они существуют в языках вроде C++ (std::vector) или Python (list). Однако Go предоставляет гораздо более мощную и безопасную альтернативу — срезы (slices), которые и выполняют роль динамических массивов в большинстве практических случаев.

Фундаментальное различие: array vs slice

// Статический массив (array) - фиксированный размер
var arr [5]int          // Массив из 5 целых чисел, размер неизменен
arr2 := [3]string{"a", "b", "c"}  // Размер 3, определен при компиляции

// Срез (slice) - "динамическое окно" в массив
var slice []int         // Срез, изначально nil
slice2 := make([]int, 0, 10)  // Срез с длиной 0, ёмкостью 10

Ключевое отличие: размер массива — часть его типа [5]int и [10]int — разные типы, а срез []int — всегда один тип независимо от размера.

Как срезы реализуют динамическое поведение

Срез — это легковесная структура данных, содержащая три компонента:

  1. Указатель на базовый массив
  2. Длину (length) — количество элементов в срезе
  3. Ёмкость (capacity) — максимальное количество элементов без переаллокации
// Создание и работа со срезом
package main

import "fmt"

func main() {
    // Создание среза с начальной ёмкостью
    nums := make([]int, 0, 3)  // length=0, capacity=3
    
    // Динамическое добавление элементов
    for i := 0; i < 10; i++ {
        nums = append(nums, i)  // Автоматическое расширение при необходимости
        fmt.Printf("len=%d cap=%d %v\n", len(nums), cap(nums), nums)
    }
    
    // При превышении ёмкости append создаёт новый массив
    // Обычно увеличивает ёмкость в 2 раза (но не гарантировано)
}

Механика "динамичности" срезов

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

  1. Выделяется новый базовый массив большего размера
  2. Существующие элементы копируются в новый массив
  3. Новый элемент добавляется в конец
  4. Старый массив собирается сборщиком мусора (если на него нет других ссылок)
// Пример демонстрации переаллокации
func demonstrateReallocation() {
    s := []int{1, 2, 3}
    fmt.Printf("До: %p, len=%d, cap=%d\n", s, len(s), cap(s))
    
    s = append(s, 4, 5, 6)  // Превышаем ёмкость
    fmt.Printf("После: %p, len=%d, cap=%d\n", s, len(s), cap(s))
    // Адрес изменился - произошла переаллокация
}

Преимущества срезов над классическими динамическими массивами

  1. Безопасность памяти: Go управляет памятью автоматически через сборщик мусора
  2. Производительность: Многие операции оптимизированы компилятором
  3. Гибкость: Богатый набор встроенных операций:
    // Разнообразные операции со срезами
    s := []int{1, 2, 3, 4, 5}
    
    part := s[1:3]        // Подсрез [2, 3]
    fullCopy := s[:]      // Копия среза (но общий массив!)
    withCapacity := s[:2:2] // Срез с ограниченной ёмкостью
    
    // Копирование
    dest := make([]int, len(s))
    copy(dest, s)          // Настоящее копирование элементов
    

Важные нюансы работы со срезами

// Пример тонкостей
func sliceGotchas() {
    // 1. Срезы разделяют память базового массива
    original := []int{1, 2, 3, 4, 5}
    slice := original[1:4]  // [2, 3, 4]
    slice[0] = 99          // Меняет и original[1]!
    
    // 2. Как избежать нежелательного разделения памяти
    safeCopy := make([]int, len(original))
    copy(safeCopy, original)  // Полная независимая копия
    
    // 3. Сравнение с nil
    var emptySlice []int      // nil-срез
    zeroLengthSlice := []int{} // Не-nil срез с длиной 0
}

Практические рекомендации

  1. Инициализация с запасом ёмкости:

    // Плохо: многократные переаллокации
    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)  // Одна аллокация
    }
    
  2. Использование ... для распаковки:

    a := []int{1, 2, 3}
    b := []int{4, 5, 6}
    combined := append(a, b...)  // Распаковка среза
    

Заключение

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

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