Slice internals in Go ↔ Java: from header to hidden allocations

Slice in Go is one of those structures that looks simple, but under the hood behaves like a little clever beast. If you are a Java developer, you might think: "well, this is just an ArrayList." And this is where the magic begins — it’s not quite so.

In this article, we will discuss: how the slice header works, why append can suddenly create a new array, how capacity grows, what aliasing is, and why copy is not just a copy, but a memory control tool.

And, of course, all this will be compared to Java: arrays and ArrayList, so you can see not only the "how," but also the "why."

copy vs append behavior

What it is and what happens under the hood

In Go, there are two fundamental ways to work with slices: append and copy. They look similar but perform fundamentally different tasks.

append adds elements to a slice. If the capacity allows — elements are simply written to the existing array. If not — the runtime creates a new array, copies the old data there, and adds the new.

copy always copies data from one slice to another. It never increases capacity and does not perform magical allocations — only copying into already allocated memory.

The equivalent in Java is a combination of ArrayList.add() and System.arraycopy(). But the key difference is: in Go, you explicitly manage copying, while append might "betray" you with hidden allocation.


package main

import "fmt"

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

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

    // As long as cap is enough — we work in the same array
    fmt.Println("before:", s, len(s), cap(s))

    s = append(s, 3) // this will trigger 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);

        // as long as capacity is enough
        System.out.println("before: " + list);

        list.add(3); // there will be a resize of the array inside ArrayList

        System.out.println("after: " + list);
    }
}
Use copy when you want controlled copying, and append when you are ready to accept possible allocation. Under the hood, append is a conditional "if it doesn't fit — recreate the array." This means: potential GC pressure, loss of references, and aliasing effects. copy, on the other hand, strictly operates within already allocated memory, making it a predictable tool.
append is ideal for building dynamic lists, streaming data, and pipelines. But if you are working with buffers (for example, network ones), copy gives control. In Java, a similar situation: ArrayList is convenient, but under heavy loads, arrays and System.arraycopy are used. The advantage of append is simplicity. The downside is hidden allocations. copy — on the contrary: more control, but more code.

slice header (ptr, len, cap)

What is it and what happens under the hood

Slice is not an array. It is a structure with three fields:


type slice struct {
    ptr *T   // pointer to the array
    len int  // length (how many elements are available)
    cap int  // capacity (how many can fit)
}

So slice is a "window" into an array. You can have multiple slices pointing to the same array.

In Java, the analog is an array + ArrayList. But ArrayList hides this structure, while slice makes it part of the way you think.


package main

import "fmt"

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

    s := arr[1:4] // ptr points to arr[1]

    fmt.Println(s)        // [2 3 4]
    fmt.Println(len(s))   // 3
    fmt.Println(cap(s))   // 4 (from arr[1] to the end of the array)
}

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

        // In Java there is no slice — only copying
        int[] sub = java.util.Arrays.copyOfRange(arr, 1, 4);

        // sub is a new array, not a view!
        System.out.println(sub.length);
    }
}
Always remember: slice is a view, not a copy. This means that changing one slice can change another. Under the hood, it is just a pointer to the same array. In Java, you are protected from this because copying happens more often. In Go — no, and this is a source of both power and bugs.
Slice header makes Go powerful for working with large data: you can pass chunks without copying. This is used in network buffers, parsers, streams. The plus — zero allocation. The minus — aliasing. In Java, copies are made more often → safer, but more expensive in memory.

slice capacity growth algorithm

What it is and what happens under the hood

When capacity runs out, Go increases it. But not linearly.

Up to ~1024 elements — usually a doubling. After that — growth by about 25%. This is a compromise between speed and memory efficiency.

In Java, ArrayList increases by about 1.5x. That is, the growth is smoother.


// pseudo-logic
if cap < 1024 {
    newCap = cap * 2
} else {
    newCap = cap * 1.25
}

// Java ArrayList
newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5x
Anticipate the size of the slice. If you know the volume — use make with capacity. Under the hood, each resize is an allocation + copying. It’s not free. In Go, this is particularly noticeable due to frequent appends.
In high-load systems (logging, streaming), it’s important to set the capacity in advance. This reduces GC pressure. In Java, the equivalent is new ArrayList(capacity). Plus — less realloc. Minus — can overestimate and waste memory.

slice reallocation

What it is and what happens under the hood

When append does not fit in capacity — a new array is created. The old one remains in memory as long as there are references to it.


// ASCII scheme

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

ptr -> new array

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

s = append(s, 3) // s now points to a new array

// t still points to the old one

import java.util.ArrayList;

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

        // a new array is created inside ArrayList when it grows
        list.add(3);
    }
}
Don’t forget: append can "detach" the slice from the old array. This breaks aliasing. Under the hood, it changes ptr. If you passed the slice to a function — it might work with a different array than you think.
This is critical when working with shared data. For example, in caching or when working with buffers. The plus — safety (new array). The minus — unexpected behavior. In Java, the analogy is resize in ArrayList, but it is hidden.

slice aliasing

What it is and what happens under the hood

Aliasing is when multiple slices point to the same array.


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

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

a[1] = 100

// b will also change!

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

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

// changes do not intersect

// ASCII scheme

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

Change in the intersection affects both
Aliasing is a strength and a trap. Under the hood it's one array. If you don't want this — use copy. It's like shared mutable state — a source of complex bugs.
Used in high-performance systems (e.g., zero-copy parsing). Plus — speed. Minus — complexity. In Java, aliasing is often avoided because copying is cheaper than bugs.

General Comparison Table

Term Go Java Comment
append vs copy append may realloc add + arraycopy Go hides allocations, Java — more explicit model
slice header ptr + len + cap hidden in ArrayList Go provides control, Java — abstraction
growth 2x → 1.25x 1.5x different trade-offs of memory vs speed
reallocation new array new array more noticeable in Go due to aliasing
aliasing exists almost none Go gives power, Java — safety

Output / Conclusion

Slice is not just a "dynamic-sized array." It is an abstraction over memory. And like any powerful abstraction, it requires respect.

Go gives you control: you see len, cap, understand when realloc happens, and can manage memory. But with that comes responsibility: aliasing, hidden allocations, unexpected copies.

Java takes a different approach: it hides the details. ArrayList resizes, copies arrays, but you rarely think about it. This reduces cognitive load but also removes control.

If you are writing high-load systems, understanding slice internals gives you an advantage. You start to see not just code, but the movement of data in memory. And it is there, in those invisible moves, that performance often hides.

The key idea: Go is about transparency and control, Java is about abstraction and safety. And the true power of a developer is knowing when one model is needed and when the other is.


🌐 На русском
Total Likes:0

Оставить комментарий

My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

From the microservice revolution to the age of efficiency
The period from 2010 to 2020 can be called an era of separation and scaling. Systems have become too large to remain monoliths. The solution has been microservices — small autonomous applications that...
Analysis of Unit testing, Race detector, Benchmarking, Profiling (pprof) Go vs Java | Testing, Debugging, and Profiling
1. Unit testing In Go, the built-in package testing allows writing unit tests. For Java developers, this is analogous to JUnit/TestNG. Unit tests in Go are simple and built into the standard libr...
Breaking down: Rate-limiter, non-blocking operations, scheduler Go vs Java | Concurrency part 4
This article is dedicated to understanding the principles of working with concurrency and synchronization in Go and Java. We will look at key approaches such as rate-limiter, non-blocking operations, ...

New Articles:

Zero Allocation in Java: what it is and why it matters
Zero Allocation — is an approach to writing code in which no unnecessary objects are created in heap memory during runtime. The main idea: fewer objects → less GC → higher stability and performance. ...
Stream vs For in Java: how to write the fastest code possible
In Java, performance is often determined not by the "beauty of the code," but by how it interacts with memory, the JIT compiler, and CPU cache. Let s analyze why the usual for is often faster than Str...
Compiler, Build, and Tooling in Go and Java: how assembly, initialization, analysis, and diagnostics are organized in two ecosystems
This article is dedicated to a general overview of how the compiler, build, and tooling practices are arranged in Go, and how to better understand them through comparison with Java. We will not delve ...
Fullscreen image