Слайсы - динамические массивы 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]
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 копирует элементы. Нет - копируется только заголовок слайса.