Slice internals в Go ↔ Java: от заголовка до скрытых аллокаций

Slice в Go — это одна из тех структур, которая выглядит простой, но под капотом ведёт себя как маленький хитрый зверь. Если ты Java-разработчик, ты можешь думать: «ну это же просто ArrayList». И вот здесь начинается магия — это не совсем так.

В этой статье мы разберём: как работает slice header, почему append может внезапно создать новый массив, как растёт capacity, что такое aliasing, и почему copy — это не просто копия, а инструмент контроля памяти.

И, конечно, всё это сравним с Java: массивами и ArrayList, чтобы ты видел не только «как», но и «почему».

copy vs append behavior

Что это такое и что происходит под капотом

В Go есть два фундаментальных способа работы с slice: append и copy. Они выглядят похожими, но выполняют принципиально разные задачи.

append добавляет элементы в slice. Если capacity (ёмкость) позволяет — элементы просто записываются в существующий массив. Если нет — runtime создаёт новый массив, копирует туда старые данные и добавляет новые.

copy же всегда копирует данные из одного slice в другой. Он никогда не увеличивает capacity и не делает магических аллокаций — только копирование в уже выделенную память.

В Java аналог — это комбинация ArrayList.add() и System.arraycopy(). Но ключевое отличие: в Go ты явно управляешь копированием, а append может «предать» тебя скрытой аллокацией.


package main

import "fmt"

func main() {
    s := make([]int, 0, 2) // len=0, cap=2

    s = append(s, 1)
    s = append(s, 2)

    // Пока cap хватает — работаем в том же массиве
    fmt.Println("before:", s, len(s), cap(s))

    s = append(s, 3) // тут произойдёт realloc!

    fmt.Println("after:", s, len(s), cap(s))
}

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(2); // capacity = 2

        list.add(1);
        list.add(2);

        // пока capacity хватает
        System.out.println("before: " + list);

        list.add(3); // произойдёт resize массива внутри ArrayList

        System.out.println("after: " + list);
    }
}
Используй copy, когда хочешь контролируемое копирование, и append — когда готов принять возможную аллокацию. Под капотом append — это условный «если не влез — пересоздай массив». Это значит: возможен GC pressure, потеря ссылок и aliasing-эффекты. copy же работает строго в рамках уже выделенной памяти, что делает его предсказуемым инструментом.
append идеально подходит для построения динамических списков, стриминга данных и пайплайнов. Но если ты работаешь с буферами (например, сетевыми), copy даёт контроль. В Java похожая ситуация: ArrayList удобен, но при высоких нагрузках используют массивы и System.arraycopy. Плюс append — простота. Минус — скрытые аллокации. copy — наоборот: больше контроля, но больше кода.

slice header (ptr, len, cap)

Что это такое и что происходит под капотом

Slice — это не массив. Это структура из трёх полей:


type slice struct {
    ptr *T   // указатель на массив
    len int  // длина (сколько элементов доступно)
    cap int  // ёмкость (сколько можно вместить)
}

То есть slice — это «окно» в массив. Ты можешь иметь несколько slice, указывающих на один и тот же массив.

В Java аналог — это массив + ArrayList. Но ArrayList скрывает эту структуру, а slice — делает её частью модели мышления.


package main

import "fmt"

func main() {
    arr := []int{1,2,3,4,5}

    s := arr[1:4] // ptr указывает на arr[1]

    fmt.Println(s)        // [2 3 4]
    fmt.Println(len(s))   // 3
    fmt.Println(cap(s))   // 4 (от arr[1] до конца массива)
}

public class Main {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};

        // В Java нет slice — только копирование
        int[] sub = java.util.Arrays.copyOfRange(arr, 1, 4);

        // sub — это новый массив, не view!
        System.out.println(sub.length);
    }
}
Всегда помни: slice — это view, а не копия. Это значит, что изменение одного slice может изменить другой. Под капотом это просто указатель на тот же массив. В Java ты защищён от этого, потому что копирование происходит чаще. В Go — нет, и это источник как силы, так и багов.
Slice header делает Go мощным для работы с большими данными: можно передавать куски без копирования. Это используется в сетевых буферах, парсерах, стримах. Плюс — нулевая аллокация. Минус — aliasing. В Java чаще делают копии → безопаснее, но дороже по памяти.

slice capacity growth algorithm

Что это такое и что происходит под капотом

Когда capacity заканчивается, Go увеличивает её. Но не линейно.

До ~1024 элементов — обычно удвоение. После — рост примерно на 25%. Это компромисс между скоростью и экономией памяти.

В Java ArrayList увеличивается примерно на 1.5x. То есть рост более плавный.


// псевдологика
if cap < 1024 {
    newCap = cap * 2
} else {
    newCap = cap * 1.25
}

// Java ArrayList
newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5x
Предугадывай размер slice. Если ты знаешь объём — используй make с capacity. Под капотом каждый resize — это аллокация + копирование. Это не бесплатно. В Go это особенно заметно из-за частых append.
В high-load системах (логирование, стриминг) важно задавать capacity заранее. Это уменьшает GC pressure. В Java аналог — new ArrayList(capacity). Плюс — меньше realloc. Минус — можно переоценить и потратить память.

slice reallocation

Что это такое и что происходит под капотом

Когда append не помещается в capacity — создаётся новый массив. Старый остаётся в памяти, пока на него есть ссылки.


// ASCII схема

Old array: [1 2]
New array: [1 2 3 0]

ptr -> новый массив

s := []int{1,2}
t := s

s = append(s, 3) // s теперь указывает на новый массив

// t всё ещё указывает на старый

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);

        // внутри ArrayList создаётся новый массив при росте
        list.add(3);
    }
}
Не забывай: append может «оторвать» slice от старого массива. Это ломает aliasing. Под капотом это смена ptr. Если ты передал slice в функцию — она может работать с другим массивом, чем ты думаешь.
Это критично при работе с shared данными. Например, при кэшировании или работе с буферами. Плюс — безопасность (новый массив). Минус — неожиданное поведение. В Java аналог — resize в ArrayList, но он скрыт.

slice aliasing

Что это такое и что происходит под капотом

Aliasing — это когда несколько slice указывают на один массив.


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

a := arr[0:2]
b := arr[1:3]

a[1] = 100

// b изменится тоже!

int[] arr = {1,2,3,4};

int[] a = java.util.Arrays.copyOfRange(arr, 0, 2);
int[] b = java.util.Arrays.copyOfRange(arr, 1, 3);

// изменения не пересекаются

// ASCII схема

arr: [1 2 3 4]
 a -> [1 2]
       b -> [2 3]

Изменение в пересечении влияет на оба
Aliasing — это сила и ловушка. Под капотом это один массив. Если ты не хочешь этого — используй copy. Это как shared mutable state — источник сложных багов.
Используется в высокопроизводительных системах (например, zero-copy парсинг). Плюс — скорость. Минус — сложность. В Java чаще избегают aliasing, потому что копирование дешевле, чем баги.

Общая таблица сравнения

Термин Go Java Комментарий
append vs copy append может realloc add + arraycopy Go скрывает аллокации, Java — более явная модель
slice header ptr + len + cap скрыто в ArrayList Go даёт контроль, Java — абстракцию
growth 2x → 1.25x 1.5x разные компромиссы памяти vs скорости
reallocation новый массив новый массив в Go заметнее из-за aliasing
aliasing есть почти нет Go даёт мощь, Java — безопасность

Вывод / Итог

Slice — это не просто «массив с динамическим размером». Это абстракция над памятью. И как любая мощная абстракция — она требует уважения.

Go даёт тебе контроль: ты видишь len, cap, понимаешь, когда происходит realloc, и можешь управлять памятью. Но вместе с этим приходит ответственность: aliasing, скрытые аллокации, неожиданные копии.

Java идёт другим путём: она прячет детали. ArrayList делает resize, копирует массивы, но ты редко думаешь об этом. Это снижает когнитивную нагрузку, но и убирает контроль.

Если ты пишешь высоконагруженные системы — понимание slice internals даёт тебе преимущество. Ты начинаешь видеть не просто код, а движение данных в памяти. И именно там, в этих невидимых перемещениях, часто скрывается производительность.

Ключевая идея: Go — про прозрачность и контроль, Java — про абстракцию и безопасность. И настоящая сила разработчика — уметь выбирать, когда нужна одна модель, а когда другая.


Всего лайков:0
Мой канал в социальных сетях
Отправляя email, вы принимаете условия политики конфиденциальности

Полезные статьи:

Рассуждение о том, почему полнота знаний недостижима и как выстроить личную архитектуру профессионального роста. Каждый разработчик хотя бы раз думал: «Как всё успеть?» Технологии растут быстрее,...
Memory, Runtime и Allocator: Сравнение Go и Java для разработчиков
В этой статье мы разберём ключевые аспекты работы с памятью, runtime и механизмами аллокации объектов в Go и Java. Мы сфокусируемся на различиях подходов к управлению памятью, работе со стеком и кучей...
Memory / Runtime / Allocator - Go vs Java
Управление памятью, указатели и профилирование — это фундаментальные аспекты эффективного кода. Рассмотрим три ключевых концепта: slice backing array, pointer и профилирование (pprof / trace), и сравн...

Новые статьи:

Go ↔ Java: Полное руководство по Runtime, памяти и аллокатору - часть 3
Эта статья — комплексное руководство по ключевым аспектам работы памяти и рантайма в Go и Java. Мы разберем фундаментальные концепции: планировщик выполнения, memory barriers, выравнивание памяти, рос...
Низкоуровневые механизмы | Go ↔ Java
В этой статье мы разберем ключевые низкоуровневые механизмы Go, сравнивая их с аналогичными инструментами в Java. Статья предназначена для Java-разработчиков, которые хотят глубже понять Go, а также д...
Scheduler internals в Go ↔ Java: как на самом деле исполняется твой код
Когда ты пишешь go func() или создаёшь Thread в Java, кажется, что ты управляешь параллельностью. Но на самом деле ты даёшь задачу планировщику — scheduler у. И вот тут начинается настоящее шоу. Go ...
Fullscreen image