Слайсы в Go: полное руководство и примеры использования

слайсы в go

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

Многие приходят из других языков, где списки (Python) или массивы (Swift) ведут себя предсказуемо. В Go слайс — это не просто динамический массив. Это прослойка над массивом со своей философией.

Как устроен массив (array) в Go?

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

Массив — это набор элементов фиксированной длины одного типа.

Структура массива в Go выглядит следующим образом:

arrays go

Способы объявления массива:

// Способ 1: Только объявление
var numbers1 [5]int // массив из 5 целых чисел [0 0 0 0 0]

// Способ 2: Полное объявление с инициализацией
var numbers2 [5]int = [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// Способ 3: Объявление с частичной инициализацией
var numbers3 [5]int = [5]int{1, 2} // [1 2 0 0 0]

// [Предпочтительно] Способ 4: Короткая запись (:=)
numbers4 := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// Способ 5: Многоточие с var
var numbers5 = [...]int{1, 2, 3, 4, 5} // длина массива 5

// Способ 6: Многоточие с :=
numbers6 := [...]int{1, 2, 3} // длина массива 3

Обычно массивы используются для хранения последовательностей фиксированной длины, например UUID:

func generateUUID() [16]byte {
   var uuid [16]byte
   // заполнение uuid...
   return uuid
}

Ключевые особенности массивов:

  1. Длина — это часть типа. Тип [5]int и [10]int — это разные, несовместимые типы.
  2. Передача по значению. Если вы передаете массив в функцию, Go скопирует весь массив. Для больших массивов это дорогая операция.

Именно из-за жесткости массивов появились слайсы. Но главное: у любого слайса всегда есть под капотом массив.

Как устроен слайс (slice) в Go?

Слайс — это структура данных, которая состоит из 3-х полей (внутреннее устройство):

type slice struct {
    ptr unsafe.Pointer // указатель на первый элемент слайса в массиве
    len int // длина (количество элементов, которые мы видим)
    cap int // емкость (количество элементов от ptr до конца массива)
}

Важно понимать разницу:

  • Длина (len) — сколько элементов можно прочитать сейчас
  • Емкость (cap) — сколько всего элементов помещается в базовый массив, на который ссылается слайс, не выделяя память.

Способы создания слайса

slices go

// 1. Через make
s := make([]int, 5, 10) // длина 5, емкость 10

// 2. Литералом
s := []string{"Go", "Python", "Swift"} // длина 3, емкость 3

// 3. Нарезкой массива
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // элементы 2, 3, 4. len = 3, cap = 4 
                  // (от индекса 1 до конца массива)

Функция append

Функция append — это единственный способ «расширить» слайс. Давайте заглянем ей под капот.

Сценарий А: Емкости достаточно (cap > len)

Если вы добавляете элемент, а в массиве под слайсом еще есть место, append просто записывает новый элемент в следующую ячейку массива и увеличивает поле len на 1. Указатель ptr не меняется.

Сценарий Б: Емкости не хватает (cap == len)

Это самый интересный момент.

  1. В памяти создается новый массив.
  2. Правило роста эмпирическое: обычное емкость удваивается для маленьких слайсов (меньше 1024 элементов), а потом растет примерно на 25%.
  3. Все старые элементы копируются из старого массива в новый.
  4. Добавляется новый элемент.
  5. Возвращается новый слайс, который указывает на новый массив.

Главный вывод: Результат append нужно присваивать обратно в переменную, потому что исходный слайс мог указывать на старый (уже неактуальный) массив.

s := make([]int, 0, 2)
s = append(s, 1) // хорошо
s = append(s, 2) // еще ок, cap = 2
s = append(s, 2) // выходим за cap, ссылка идет уже на новый массив

Полезные практики работы со слайсами

Накопив опыт, сообщество Go выработало несколько полезных паттернов при работе со слайсами.

Лишние аллокации памяти

Всегда старайтесь задавать емкость (cap), если примерно знаешь размер. Это избавит от лишних аллокаций памяти.

// Плохо: append будет много раз перевыделять память
data := []int{}
for i := 0; i < 1000; i++ {
    data = append(data, i)
}

// Хорошо: сразу выделили массив на 1000 элементов
data := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    data = append(data, i)
}

Создание независимых копий

Если нужно создать независимые копии, лучше всего использовать функцию copy(). Простое присваивание скопирует структуру слайса, но указатель будет вести на тот же базовый массив (изменения в a затронут изменения в b).

src := []int{1, 2, 3}
// важно: длина должна быть достаточной
dst := make([]int, len(src))
copy(dst, src)
// теперь dst и src независимы

Проверка на пустоту

Проверять слайс на пустоту лучше всего через функцию вычисления длины слайса.

// Плохо: так как проверяется nil, а не длина
var s1 []int
fmt.Println(s1 == nil) // true

s2 := []int{}
fmt.Println(s2 == nil) // false

// Хорошо: так как проверяется количество элементов
fmt.Println(len(s1) == 0) // true
fmt.Println(len(s2) == 0) // true

Идиома Remove

Удалять элементы из слайса лучше всего без утечек в памяти:

i := 2 // Индекс удаляемого элемента
a = append(a[:i], a[i+1]...)
// Если в слайсе указатели, чтобы избежать утечки,
// лучше обнулить последний элемент перед изменением длины
// copy(a[i:], a[i+1:])
// a[len(a)-1] = nil // или zero value
// a = a[:len(a)-1]

Подводные камни и опасности

Камень 1: «Невидимое» изменение данных

Создав под-слайс, нужно помнить, что слайс и под-слайс ссылаются на один и тот же массив.

original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3]
sub[0] = 999
fmt.Println(original) // [1, 999, 3, 4, 5] - УПС! Исходный массив изменился!

Решение: Если нужна полная независимость, используйте copy или выражение original[1:3:3] (полный срез с контролем емкости).

Камень 2: Append в под-слайсе

Это один из самых опасных моментов для новичков.

s1 := []int{1, 2, 3, 4}
s2 := s1[:2] // [1,2], cap=4
s2 = append(s2, 5) // вместо 3 становится 5? Да, cap хватает, пишем в массив.

fmt.Println(s1) // [1, 2, 5, 4] - массив испортился
fmt.Println(s2) // [1, 2, 5]

Можно подумать, что s2 расширяется, а на самом деле он просто перезаписывает данные в общем массиве.

Пишем свою реализацию append()

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

package main

import "fmt"

// SomeAppend принимает слайс и 
// переменное число элементов для добавления
func SomeAppend(slice []int, items ...int) []int {
    // Сначала узнаем, сколько места нам нужно
    needed := len(slice) + len(items)

    // Если текущей емкости не хватает
    if needed > cap(slice) {
        // Создаем новый массив.
        // Для простоты удвоим емкость или возьмем needed,
        // если нужно больше.
        newCap := cap(slice) * 2
        if newCap < needed {
            newCap = needed
        }

        // Создаем новый слайс (новый массив)
        newSlice := make([]int, needed, newCap)

        // Копируем старые данные
        copy(newSlice, slice)

        // Копируем новые данные (items) в конец
        copy(newSlice[len(slice):], items)
        return newSlice
    }

    // Если емкости хватает - работаем в текущем массиве
    // Создаем новый слайс, указывающий на тот же массив,
    // но с обновленной длиной
    newSlice := slice[:needed]

    // Копируем элементы в конец
    copy(newSlice[len(slice):], items)
    return newSlice
}

func main() {
    // Тест 1: Не хватает емкости
    a := []int{1, 2}
    fmt.Printf("До SomeAppend: len=%d cap=%d %v\n", len(a), cap(a), a)
    a = SomeAppend(a, 3, 4, 5, 6)
    fmt.Printf("После: len=%d cap=%d %v\n\n", len(a), cap(a), a)

    // Тест 2: Хватает емкости
    b := make([]int, 2, 5)
    b[0] = 10
    b[1] = 20
    fmt.Printf("До SomeAppend: len=%d cap=%d %v\n", len(b), cap(b), b)
    b = SomeAppend(b, 30, 40)
    fmt.Printf("После: len=%d cap=%d %v\n", len(b), cap(b), b)
}

Эта реализация наглядно показывает логику рантайма Go: либо мы пишем в существующий массив, либо аллоцируем новый и копируем все туда.

Ключевой вывод

Надеюсь, что данное руководство помогло вам разобраться в слайсах глубже. Как только вы начнете мыслить не просто абстрактным списком, а представлять лежащий в основе массив с его длиной и емкостью, — слайсы просто станут удобным и предсказуемым инструментом

Был ли у вас случай, когда append или работа со слайсами приводила к неожиданному багу? Делитесь своим опытом в комментариях.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: