October 24, 2025

Slice в Golang. BASHNYA_EDU

Навигация:

  1. Повторяем теорию по слайсам.
  2. Передача слайсов в функцию по значению.
  3. Передача слайса внутрь функции по указателю.
  4. Передача слайса внутрь функции по значению с правильным копированием.
  5. Вывод.

1) Что такое slice в Go?

Введение

В первую очередь, slice — это структура.
У этой структуры есть три ключевых поля:

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

Получается, что слайс — это надстройка (или «обёртка») над обычным статическим массивом.
В целом, реализация слайса чем-то схожа с реализацией std::vector в C++.


Что такое len и cap?

  • len — фактическое количество элементов в нашем нижележащем массиве.
  • cap — вместимость массива, то есть максимальное количество элементов, которое он может содержать без перевыделения памяти.

Зачем нужен cap?
Чтобы мы могли добавлять элементы в слайс без немедленного перевыделения памяти. Ведь любое выделение памяти - системный вызов, а системные вызовы дорогие.


Что произойдет, если len станет равен cap?

Если len == cap и мы добавляем элемент через append, под капотом произойдёт следующее:

  1. Go создаст новый статический массив, большего размера.
  2. Старые элементы будут скопированы в новый массив.
  3. Новый cap вычисляется как oldCap * coefficient.

Откуда берётся коэффициент роста?

Под капотом Go есть функция, которая на основе текущего количества элементов определяет коэффициент увеличения.
Она выглядит примерно так (упрощённый пример из исходников Go):

// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
    newcap := oldCap
    doublecap := newcap + newcap
    if newLen > doublecap {
        return newLen
    }
    const threshold = 256
    if oldCap < threshold {
        return doublecap
    }
    for {
        // Transition from growing 2x for small slices
        // to growing 1.25x for large slices. This formula
        // gives a smooth-ish transition between the two.
        newcap += (newcap + 3*threshold) >> 2
        if uint(newcap) >= uint(newLen) {
            break
        }
    }
    if newcap <= 0 {
        return newLen
    }
    return newcap
}


2) Передача слайсов в функцию по значению

Рассмотрим пример:

func main() {
    slice := []int{11, 12, 13}
    test(slice)

    // 1) Что выведет эта функция?
    fmt.Println(slice)
}

func test(slice []int) {
    slice[1] = 68
    slice = append(slice, 14)
    slice[0] = 101

    // 2) Что выведет эта функция?
    fmt.Println(slice)
}

Попробуй сам

Прежде чем читать дальше — попробуй самостоятельно определить, что выведет программа,
а уже потом сверяйся с ответом ниже 👇

Ответ

1) В main() будет выведено: [11 68 13]

2) В test() будет выведено: [101 68 13 14]


Пояснение

При передаче слайса в функцию по значению (то есть test(slice)), происходит копирование структуры слайса.
Это важно: копируется именно структура слайса, а не данные массива.

В Go у слайса нет конструктора копирования, поэтому:

  • копируется указатель на нижележащий массив,
  • копируются значения len и cap.

То есть на момент вызова функции test у нас есть две разные структуры (slice в main и slice в test),
но обе они указывают на один и тот же участок памяти.


3) Передача слайса внутрь функции по указателю

Рассмотрим пример:

func main() {
    slice := []int{11, 12, 13}
    test(&slice)

    // 1) Что выведет эта функция?
    fmt.Println(slice)
}

func test(slicePtr *[]int) {    
    (*slicePtr)[1] = 68    
    *slicePtr = append(*slicePtr, 14)    
    (*slicePtr)[0] = 101    
    
    // 2) Что выведет эта функция?    
    fmt.Println(*slicePtr)
}

Попробуй сам

Прежде чем читать дальше — попробуй самостоятельно определить, что выведет программа,
а уже потом сверяйся с ответом ниже 👇

Ответ

1) В main() будет выведено: [101 68 13 14]

2) В test() будет выведено: [101 68 13 14]


Пояснение

При передаче слайса в функцию по указателю (то есть test(&slice)), происходит копирование адреса структуры слайса.
Это важно: копируется именно адрес структура слайса, а не структура слайса.

То есть на момент вызова функции test у нас есть структура слайс в main и указатель на эту структуру в функции test.


4) Передача слайса внутрь функции по значению с правильным копированием

Чтобы передать слайс в функцию и не переживать за работу с памятью, пользуйтесь функцией copy(dest, src)

Пример:

func main() {
    slice := []int{11, 12, 13}
    copySlice := []int{}
    copy(copySlice, slice)
    test(copySlice)

    // 1) Что выведет эта функция?
    fmt.Println(slice)
}

func test(slice []int) {
    slice[1] = 68
    slice = append(slice, 14)
    slice[0] = 101

    // 2) Что выведет эта функция?
    fmt.Println(slice)
}

Попробуй сам

Прежде чем читать дальше — попробуй самостоятельно определить, что выведет программа,
а уже потом сверяйся с ответом ниже 👇

Ответ

1) В main() будет выведено: [11 12 13]

2) В test() будет выведено: [101 68 13 14]


Пояснение

При использовании функции copy, мы создаём копию слайса, НО ВАЖНО заметить, что внутри этой копии будет выделен отдельный массив изначально, там не будет копироваться адрес, там именно скопируются элементы старого массива в новый.


5) Вывод

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

Для меня все эти принципы — как рефлекс.
Я однажды выучил их и с тех пор держу в голове.
А повторение во время лекций и менторства помогает не расслабляться и быть всегда в форме.


🗣️ Пишите вопросы, отзывы, комментарии!
Насколько понятно удалось объяснить, что происходит «под капотом» в разных ситуациях?
Если заметили неточность или ошибку — исправляйте, спорьте, обсуждайте.