Coșul fermecat cu mere: lungimea, capacitatea și elementele „fantomă” din slice-urile Go

Cod Go care creează un slice cu 7 elemente dintr-un slice cu 5 elemente
Imagine: Răzvan Sima
Aceasta este traducerea în română a unui articol publicat inițial în engleză pe Stackademic (Medium): The Magical Apple Basket and Go Slice Mysteries.

O ghicitoare cu mere

Ai un coș cu 10 mere. Iei 5 dintre ele și le pui într-un coș mai mic. Și acum partea ciudată: cum ajungi să ai 7 mere într-un al treilea coș, dacă în al doilea ai pornit doar cu 5? Pare imposibil, dar în Go se poate. Să vedem cum.

Coșul inițial

Mai întâi facem coșul mare cu mere, adică array-ul inițial.

// Coșul inițial, cu 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 (un array)
firstBasketOfApples := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Al doilea coș

Luăm 5 mere din coșul mare și le punem într-un al doilea coș, mai mic. În Go, acest al doilea coș se numește „slice”.

// Al doilea coș, cu 🍎🍎🍎🍎🍎 din primul (un slice)
secondBasketOfApples := firstBasketOfApples[:5]
fmt.Printf("secondBasketOfApples it has %d elements and components: %v \n", len(secondBasketOfApples), secondBasketOfApples)
// Output:
//       secondBasketOfApples it has 5 elements and components: [1 2 3 4 5]

Al treilea coș și 🪄✨

Acum vine scamatoria. Din coșul mic, cu 5 mere, facem un al treilea coș cu 7 mere.

// Scamatoria: un slice nou cu 🍎🍎🍎🍎🍎🍎🍎
// făcut doar din secondBasketOfApples, care are 🍎🍎🍎🍎🍎
thirdBasketOfApples := secondBasketOfApples[0:7]
fmt.Printf("thirdBasketOfApples it has %d elements and components: %v \n", len(thirdBasketOfApples), thirdBasketOfApples)
// Output:
//      thirdBasketOfApples it has 7 elements and components: [1 2 3 4 5 6 7]

Dezvăluim trucul: anatomia unui slice

Ca să înțelegem „magia”, ne uităm din ce e făcut un slice în Go. Are trei componente:

  1. un pointer către primul element din array-ul din spate;
  2. lungimea (len): câte elemente are slice-ul;
  3. capacitatea (cap): până la ce lungime poate crește slice-ul fără să aibă nevoie de un array nou.

Structura e definită în pachetul runtime, în fișierul slice.go:

// Structura internă a unui slice în Go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

Legătura cu array-ul din spate

Slice-ul secondBasketOfApples a păstrat un pointer către primul element al array-ului (firstBasketOfApples[0]), lungimea 5 și capacitatea 10, adică mărimea array-ului original.

Cum thirdBasketOfApples e făcut din secondBasketOfApples, își poate întinde lungimea până la capacitatea array-ului original, care e 10. De asta am putut pune două mere „în plus” în thirdBasketOfApples.

Riscuri: partea întunecată a slice-urilor

Dacă nu știi că un slice „copil” se poate întinde dincolo de lungimea slice-ului „părinte”, poți avea mai multe probleme:

  • Date expuse. Slice-ul copil poate ajunge la date pe care n-ar trebui să le vadă. Asta poate fi o problemă de securitate sau o sursă de erori de logică.
  • Modificări nedorite. Ce schimbi în slice-ul copil schimbă array-ul din spate și, implicit, toate celelalte slice-uri care îl folosesc. Rezultatul: comportament imprevizibil și bug-uri greu de urmărit.
  • Depanare grea. Problemele de felul ăsta se găsesc greu. De cele mai multe ori codul merge bine și cedează subtil doar când condițiile sunt „exact cum trebuie”.
  • Panic dincolo de capacitate. Poți întinde un slice doar până la capacitatea lui. Dacă încerci mai mult, de exemplu secondBasketOfApples[0:11], programul se oprește cu panic. Doar append alocă un array nou atunci când capacitatea nu mai ajunge.
  • Confuzie la mentenanță. Pentru cine nu știe cum funcționează slice-urile pe dinăuntru, comportamentul ăsta face codul greu de înțeles și de întreținut.

Limitarea capacității: gata cu magia

Dacă vrei să oprești comportamentul ăsta, folosește notația [x:y:z] când creezi slice-ul nou:

// Limităm capacitatea slice-ului nou
limitedSlice := firstBasketOfApples[0:5:5]

Cu [x:y:z], capacitatea slice-ului nou e limitată la z - x, deci nu mai poți adăuga mere „în plus” din array-ul original. Notația e descrisă în specificația limbajului, la „full slice expressions”.

Dacă nu folosești [x:y:z], verifică întotdeauna lungimea și capacitatea cu len() și cap(), ca să fii sigur că nu depășesc valorile la care te aștepți.

Slice-urile din Go sunt puternice și flexibile, dar merită să știi ce se întâmplă sub capotă. Așa scrii cod Go mai sigur, mai eficient și mai ușor de întreținut.


Actualizare 2026

Note adăugate la traducere; nu există în articolul original din 2023. Actualizate pentru Go 1.27, versiunea curentă la data publicării.

append pe copil scrie peste părinte. E unul dintre cele mai frecvente bug-uri legate de slice-uri. Dacă slice-ul copil mai are capacitate, append nu alocă nimic, ci scrie direct în array-ul părintelui:

parent := []int{1, 2, 3, 4, 5}
child := parent[:2]
child = append(child, 99)
fmt.Println(parent) // [1 2 99 4 5], elementul 3 a dispărut

Soluția e aceeași notație din articol, parent[:2:2], sau slices.Clip (Go 1.21), care face exact asta. Cu capacitatea tăiată, append e obligat să aloce un array nou, iar părintele rămâne neatins.

Capacitatea nu e întotdeauna mărimea array-ului. În exemplul cu merele capacitatea iese 10 pentru că slice-ul începe de la indexul 0. Regula generală: capacitatea unui slice a[low:high] e len(a) - low. De exemplu, firstBasketOfApples[3:5] are lungimea 2, dar capacitatea 7.

Un slice mic poate ține în viață un array mare. Cât timp există un slice care indică spre un array, garbage collector-ul nu poate elibera array-ul. Dacă păstrezi 10 octeți dintr-un buffer de 10 MB, ții în memorie tot bufferul. Când ai nevoie doar de o bucată mică pe termen lung, fă o copie cu slices.Clone.

Cea mai bună explicație oficială a acestor mecanisme rămâne articolul Go Slices: usage and internals de pe blogul Go.


La DO IT MAGIC SOFTWARE scriem în Go sisteme care rulează în producție, de la platforme web la soluții AI locale. Dacă ai un proiect Go care are nevoie de o a doua opinie, scrie-ne.