Slices are headers, maps are pointers
A Go slice is not a list. It is a three-word header: {pointer, len, cap}. Copying a slice copies the header,
not the elements — which is exactly the ArrayList aliasing you already know, plus a cap nobody told you about.
package main
import "fmt"
func main() {
xs := make([]int, 3, 10) // len 3, cap 10
ys := xs // header copy — same backing array
ys[0] = 42
fmt.Println(xs[0]) // 42: aliasing, same as Java
fmt.Println(len(xs), len(ys), cap(xs))
fmt.Println(&xs[0] == &ys[0]) // true — identical storage
}
var xs = new ArrayList<>(List.of(0, 0, 0));
var ys = xs; // same object, same aliasing
ys.set(0, 42); // xs.get(0) == 42
int[] arr = new int[3]; // fixed size: no capacity concept, no growth
const xs = [1, 2, 3];
const ys = xs; // same array reference
ys[0] = 42; // xs[0] === 42
ys.push(4); // length grows in place — no separate cap
append returns a new header
append writes into spare capacity if it can, and allocates a copy when it cannot. Both outcomes are legal, which
is why the result must be assigned:
package main
import "fmt"
func main() {
base := make([]int, 3, 10) // len 3, cap 10
a := append(base, 1) // fits → same array
b := append(base, 2) // fits → same array again
fmt.Println(a[3], b[3]) // 2 2 — a[3] was overwritten
full := make([]int, 3) // len == cap
c := append(full, 1) // no spare capacity → new array
d := append(full, 2) // new array again
fmt.Println(c[3], d[3]) // 1 2 — independent
}
List<Integer> base = new ArrayList<>(List.of(0, 0, 0)); // capacity managed for you
base.add(1); // never aliases silently; growth is invisible
List<Integer> copy = List.copyOf(base); // explicit copy
const base = [0, 0, 0];
const a = [...base]; // explicit spread = real copy
Trap: passing a subslice to a goroutine and appending to it inside that goroutine is a data race on the shared array.
xs[:len(xs):len(xs)](three-index slice) caps it soappendis forced to copy.
Maps: reference type, no order, no concurrency
package main
import "fmt"
type Account struct{ Balance int }
func main() {
m := map[string]*Account{"a": {Balance: 10}}
alias := m // same hash table, like Java
alias["b"] = &Account{Balance: 99}
fmt.Println(len(m)) // 2
v, ok := m["missing"] // comma-ok, the Go Optional
fmt.Println(v, ok)
m["a"].Balance += 5 // fine: pointer value
fmt.Println(m["a"].Balance)
delete(m, "b")
fmt.Println(len(m))
}
Map<String, Account> m = new HashMap<>();
m.put("a", new Account(10));
Account v = m.get("missing"); // null — no comma-ok, use getOrDefault/containsKey
m.get("a").balance += 5; // objects are references
const m = new Map<string, Account>([["a", { balance: 10 }]]);
const v = m.get("missing"); // undefined
m.get("a")!.balance += 5; // optional chaining or a non-null assertion
Three facts that bite Java developers:
- Iteration order is randomised on purpose. No
LinkedHashMapby default; sort keys yourself. - Concurrent writes are a fatal runtime error, not a caught exception — you cannot
recoverfrom it. Usesync.Mutex,sync.Map(for read-mostly caches), or a channel. - A struct value is not addressable inside the map:
m["a"].Balance++fails to compile formap[string]Account. Store pointers, or read-modify-write.
What replaces which
| Java | Go |
|---|---|
ArrayList<T> |
[]T (with explicit cap) |
HashMap<K,V> |
map[K]V |
ConcurrentHashMap |
sync.Map or map + sync.RWMutex |
Collections.unmodifiableList |
copy the slice (there is no read-only slice) |
Arrays.asList(...) |
[]T{...} |
stream filter/map |
a for loop (the honest answer) |
Proof checkpoint
- Make a slice
s := make([]int, 0, 4), append 4 items, thent := append(s, 5)and prove whethertshares storage withs(hint:&t[0] == &s[0]). - Write a map read that distinguishes "absent" from "present with zero value".
- Explain in one sentence why
capexists at all (spoiler: allocation control, not safety).
stored in this browser only