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 — про абстракцию и безопасность. И настоящая сила разработчика — уметь выбирать, когда нужна одна модель, а когда другая.
Галерея
Полезные статьи:
Новые статьи: