Слайсы - динамические массивы Go

Если массивы в Go - это фундамент, то слайсы - это рабочая лошадка. В 99% случаев вы будете использовать именно слайсы.

Массивы vs Слайсы

Массивы - фиксированный размер

// Массив - размер часть типа
var arr1 [5]int
arr2 := [3]string{"Go", "PHP", "JS"}

// Размер нельзя изменить
fmt.Println(len(arr1)) // 5 навсегда

// Массивы копируются при присваивании
arr3 := arr1 // полная копия

Слайсы - динамический размер

// Слайс - без указания размера
var slice1 []int
slice2 := []string{"Go", "PHP", "JS"}

// Можно менять размер
slice1 = append(slice1, 1, 2, 3)
fmt.Println(len(slice1)) // 3
Слайсы - это ссылочный тип. При присваивании копируется только дескриптор, а не данные.

Создание слайсов

Литерал слайса

// Пустой слайс
empty := []int{}

// С начальными значениями
numbers := []int{1, 2, 3, 4, 5}

Make - создание с емкостью

// make([]T, length, capacity)
slice := make([]int, 5)      // len=5, cap=5
slice2 := make([]int, 5, 10) // len=5, cap=10

fmt.Println(len(slice2)) // 5
fmt.Println(cap(slice2)) // 10

Из массива

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:]   // весь массив
slice2 := arr[1:4] // элементы с индексами 1, 2, 3

Операции со слайсами

Append - добавление элементов

var nums []int
nums = append(nums, 1)        // [1]
nums = append(nums, 2, 3, 4)  // [1 2 3 4]

// Объединение слайсов
more := []int{5, 6, 7}
nums = append(nums, more...) // [1 2 3 4 5 6 7]

Slicing - создание под-слайсов

slice := []int{1, 2, 3, 4, 5}

sub1 := slice[1:4]  // [2 3 4]
sub2 := slice[:3]   // [1 2 3]
sub3 := slice[2:]   // [3 4 5]
sub4 := slice[:]    // [1 2 3 4 5]
slice[start:end] - включает start, но НЕ включает end. Аналогично `array_slice($arr, $start, $end - $start)` в [PHP](../php/04-arrays.md).

Copy - копирование

src := []int{1, 2, 3}
dst := make([]int, len(src))

n := copy(dst, src) // возвращает количество скопированных элементов
fmt.Println(n, dst) // 3 [1 2 3]

Внутреннее устройство

Слайс - это структура из трех полей:

type slice struct {
    array unsafe.Pointer // указатель на массив
    len   int           // текущая длина
    cap   int           // емкость
}

Демонстрация

s1 := make([]int, 3, 5) // len=3, cap=5
s2 := s1[:4]            // расширяем до 4

fmt.Printf("s1: len=%d cap=%d %v\n", len(s1), cap(s1), s1)
fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)

// Изменения видны в обоих слайсах!
s2[0] = 99
fmt.Println(s1[0]) // 99

Типичные ошибки и ловушки

1. Append может создать новый массив

s1 := []int{1, 2, 3}
s2 := s1

s2 = append(s2, 4) // может создать новый массив
s2[0] = 99

fmt.Println(s1) // [1 2 3] - не изменился!
fmt.Println(s2) // [99 2 3 4]

2. Утечка памяти при sub-slicing

// Плохо: держим в памяти весь большой слайс
bigSlice := make([]byte, 1000000)
smallSlice := bigSlice[:10] // держит ссылку на весь миллион байт

// Хорошо: копируем нужное
smallSlice := make([]byte, 10)
copy(smallSlice, bigSlice[:10])

3. Nil slice vs empty slice

var nilSlice []int      // nil
emptySlice := []int{}   // не nil, но len=0

fmt.Println(nilSlice == nil)   // true
fmt.Println(emptySlice == nil) // false

// Но оба ведут себя одинаково с len и append
fmt.Println(len(nilSlice))     // 0
fmt.Println(len(emptySlice))   // 0

Практические примеры

Фильтрация

func filter(nums []int, predicate func(int) bool) []int {
    result := make([]int, 0, len(nums)) // оптимизация: выделяем capacity

    for _, n := range nums {
        if predicate(n) {
            result = append(result, n)
        }
    }

    return result
}

// Использование
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := filter(numbers, func(n int) bool { return n%2 == 0 })
// [2 4 6 8 10]

Удаление элемента

// Удаление по индексу (сохраняя порядок)
func remove(slice []int, index int) []int {
    return append(slice[:index], slice[index+1:]...)
}

// Удаление по индексу (быстро, но меняет порядок)
func removeUnordered(slice []int, index int) []int {
    slice[index] = slice[len(slice)-1]
    return slice[:len(slice)-1]
}

Реверс слайса

func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

Многомерные слайсы

// Создание матрицы
rows, cols := 3, 4
matrix := make([][]int, rows)
for i := range matrix {
    matrix[i] = make([]int, cols)
}

// Инициализация
matrix[0][0] = 1
matrix[1][2] = 5

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

Pre-allocation

// Плохо: много реаллокаций
var result []string
for _, item := range items {
    if condition(item) {
        result = append(result, process(item))
    }
}

// Хорошо: выделяем память заранее
result := make([]string, 0, len(items))
for _, item := range items {
    if condition(item) {
        result = append(result, process(item))
    }
}

Переиспользование слайсов

// Очистка слайса для переиспользования
slice = slice[:0] // сохраняет capacity

// Полная очистка (освобождение памяти)
slice = nil

Полезные паттерны

Stack (стек)

type Stack []int

func (s *Stack) Push(v int) {
    *s = append(*s, v)
}

func (s *Stack) Pop() int {
    if len(*s) == 0 {
        panic("empty stack")
    }

    index := len(*s) - 1
    value := (*s)[index]
    *s = (*s)[:index]

    return value
}

Queue (очередь)

type Queue []int

func (q *Queue) Enqueue(v int) {
    *q = append(*q, v)
}

func (q *Queue) Dequeue() int {
    if len(*q) == 0 {
        panic("empty queue")
    }

    value := (*q)[0]
    *q = (*q)[1:]

    return value
}

<ComparisonTable data={{ headers: ["Операция", "Сложность", "Комментарий"], rows: [ ["append", "O(1) амортизированное", "O(n) при реаллокации"], ["copy", "O(n)", "n - количество элементов"], ["slice[i]", "O(1)", "Прямой доступ"], ["slice[i:j]", "O(1)", "Создание под-слайса"], ["len/cap", "O(1)", "Просто чтение поля"] ] }} />

Итоги

  • Слайсы - основная структура данных в Go
  • Понимайте разницу между len и cap
  • Помните про общий underlying array
  • Используйте pre-allocation для производительности
  • copy() для настоящего копирования

В следующем уроке изучим maps - хеш-таблицы в Go!

Типичная ошибка

Думать, что a = b копирует элементы. Нет - копируется только заголовок слайса.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.