Aceasta este traducerea în română a unui articol publicat inițial în engleză pe Medium: Shadow Copy vs. Deep Copy in Go: What You Need to Know.
În dezvoltarea software, una dintre cele mai frecvente operații este copierea datelor. Dar atenție: nu toate copiile sunt la fel.
În Go (Golang) există două tipuri principale de copiere: copierea superficială (shallow copy, numită uneori și shadow copy) și copierea profundă (deep copy). Diferența contează atunci când vrei să lucrezi cu datele în mod sigur și eficient.
Înainte să intrăm în detaliile copierii în Go, trecem pe scurt prin conceptele și tipurile de date pe care le folosim în articol. Dacă știi deja Go, poți sări peste această parte. Textele afișate de programe au rămas în engleză, ca în original.
Struct-uri
Struct-urile grupează mai multe valori într-o singură entitate. Pot conține variabile de orice tip și sunt esențiale pentru modelarea obiectelor din lumea reală în cod.
type Person struct {
Name string
Age int
}
Slice-uri
În Go, un slice este ca o fereastră flexibilă prin care vezi o parte dintr-un array. Array-urile au o dimensiune fixă, care nu se mai poate schimba după creare, în timp ce slice-urile sunt dinamice: poți adăuga sau elimina elemente. Important de reținut: un slice are întotdeauna în spate un array.
package main
import "fmt"
func main() {
// Creăm un slice inițial, originalSlice.
originalSlice := []int{1, 2, 3, 4, 5}
fmt.Println("Original Slice:", originalSlice)
// Creăm un slice nou din originalSlice.
// newSlice va folosi același array ca originalSlice.
newSlice := originalSlice[1:4]
fmt.Println("New Slice:", newSlice)
// Modificăm un element din newSlice.
// Pentru că newSlice împarte același array cu originalSlice,
// modificarea le afectează pe amândouă.
newSlice[0] = 99
// Afișăm ambele slice-uri după modificare.
fmt.Println("Modified New Slice:", newSlice)
fmt.Println("Original Slice after modification:", originalSlice)
// Modificarea lui newSlice a schimbat și originalSlice,
// pentru că ambele folosesc același array.
}
Pointeri
Pointerii sunt variabile care stochează adresa altei variabile. În Go, pointerii se folosesc pentru a modifica variabila spre care indică sau pentru a evita copierea unor valori mari.
package main
import "fmt"
func main() {
var x int = 5
var p *int
p = &x
fmt.Println(p)
}
În Go, atribuirea copiază întotdeauna valoarea. La un struct se copiază fiecare câmp în parte. Pentru slice-uri, map-uri și pointeri însă, valoarea copiată este doar descriptorul sau adresa, nu datele în sine: copia și originalul ajung să folosească aceleași date. Asta este o copie superficială.
Prin contrast, un array (de exemplu [3]string) se copiază complet la atribuire, iar string-urile sunt imutabile, deci copierea lor este întotdeauna sigură.
package main
import "fmt"
type Car struct {
Brand string
Model string
Owners []string
}
func main() {
car1 := Car{Brand: "Tesla", Model: "Model X", Owners: []string{"Alice", "Bob"}}
car2 := car1 // copie superficială
// schimbăm elementul 0 din Owners pe copie
car2.Owners[0] = "John"
// și Owners din car1 s-a schimbat
fmt.Printf("car1 :%v , Car2: %v", car1, car2)
}
Problema copierii superficiale: modificarea elementelor din car2.Owners se vede și în car1.Owners, pentru că ambele indică spre același array. Asta poate produce erori greu de depistat.
Atenție: doar modificarea elementelor se propagă. Dacă reatribui câmpul (car2.Owners = []string{"John"}), car1 nu este afectat.
Avantajul copierii superficiale: este rapidă și consumă puțină memorie.
O copie profundă creează o instanță nouă a variabilei și copiază toate elementele interne, indiferent cât de „adânc” se află în structura de date.
Poți folosi un pachet existent, dar personal prefer un mic truc: copierea profundă prin json.Marshal și json.Unmarshal.
package main
import (
"encoding/json"
"fmt"
)
type Engine struct {
Capacity int
Power int
}
type Car struct {
Brand string
Model string
Owners []string
Engine Engine
}
func deepCopy(src, dst any) error {
bytes, err := json.Marshal(src)
if err != nil {
return err
}
return json.Unmarshal(bytes, dst)
}
func main() {
car1 := Car{
Brand: "Tesla",
Model: "Model X",
Owners: []string{"Alice", "Bob"},
Engine: Engine{Capacity: 100, Power: 300},
}
var car2 Car
// Facem copia profundă
if err := deepCopy(&car1, &car2); err != nil {
fmt.Println("copy failed:", err)
return
}
// Modificăm primul element din Owners și motorul în car2
car2.Owners[0] = "John"
car2.Engine.Capacity = 200
fmt.Printf("Car1: %v\nCar2: %v\n", car1, car2)
// Acum cele două sunt diferite
fmt.Printf("Car engine: %v\nCar2: %v\n", car1.Engine, car2.Engine)
}
Poți rula codul în Go Playground.
Soluția are însă câteva limitări:
chan, func și unsafe.Pointer.O soluție mai bună este o implementare manuală pentru tipul tău de date, de exemplu:
package main
import (
"fmt"
)
type Car struct {
Brand string
Model string
Owners []string
}
// deepCopyCar face o copie profundă din src în dest.
func deepCopyCar(src, dest *Car) {
dest.Brand = src.Brand
dest.Model = src.Model
// Creăm un slice nou, cu propriul array
dest.Owners = make([]string, len(src.Owners))
// Copiem conținutul slice-ului original în cel nou
copy(dest.Owners, src.Owners)
}
func main() {
car1 := &Car{Brand: "Tesla", Model: "Model X", Owners: []string{"Alice", "Bob"}}
var car2 Car
// Facem copia profundă manual
deepCopyCar(car1, &car2)
// Modificăm primul element din Owners în car2
car2.Owners[0] = "John"
fmt.Printf("Car1: %v\nCar2: %v\n", car1, car2)
// Acum cele două sunt diferite
}
Observație: make creează un slice gol chiar dacă originalul era nil, așa că un Owners nesetat devine [] în JSON în loc de null. Varianta care păstrează nil este în secțiunea „Actualizare 2026” de mai jos.
Problema copierii profunde: poate costa timp de procesor și memorie.
Avantajul copierii profunde: obții o copie complet izolată de datele originale.
Fiecare tip de copiere are avantaje și dezavantaje, în funcție de cerințele proiectului. Dacă înțelegi ce se copiază efectiv la o atribuire în Go și ce rămâne comun, eviți erori subtile și consumul inutil de memorie, iar aplicațiile tale devin mai robuste.
Secțiune adăugată la traducere; nu există în articolul original din 2023. Actualizată pentru Go 1.27, versiunea curentă la data publicării.
1. append poate rupe legătura dintre slice-uri. Un slice are lungime (len) și capacitate (cap). Cât timp append încape în capacitate, copia și originalul folosesc același array. Când capacitatea e depășită, Go alocă un array nou și legătura dispare. De aici vine cea mai greu de reprodus variantă a bug-ului: uneori modificarea se vede în original, alteori nu.
a := make([]int, 3, 4) // len 3, cap 4
b := a
b = append(b, 4) // încape în capacitate: același array
b[0] = 99
fmt.Println(a[0]) // 99
b = append(b, 5) // depășește capacitatea: array nou
b[0] = 1
fmt.Println(a[0]) // tot 99, a nu mai este afectat
2. Map-urile sunt cea mai frecventă capcană. Un map dintr-un struct copiat este comun ambelor copii. Din Go 1.21 îl copiezi cu maps.Clone.
type Config struct {
Labels map[string]string
}
c1 := Config{Labels: map[string]string{"env": "prod"}}
c2 := c1
c2.Labels["env"] = "dev"
fmt.Println(c1.Labels["env"]) // dev: c1 a fost modificat
c2.Labels = maps.Clone(c1.Labels) // acum c2 are propriul map
3. Varianta idiomatică: o metodă Clone. În loc de o funcție cu doi pointeri, o metodă cu value receiver returnează copia. slices.Clone (Go 1.21) păstrează nil dacă originalul era nil.
func (c Car) Clone() Car {
c.Owners = slices.Clone(c.Owners)
return c
}
car2 := car1.Clone()
Atenție: slices.Clone și maps.Clone copiază elementele prin atribuire. Pentru []string e suficient, dar pentru []*Engine sau [][]int elementele interioare rămân comune și trebuie copiate și ele.
4. Copii defensive la granițele API-ului. O metodă care returnează un câmp de tip slice sau map dă apelantului acces direct la starea internă. Returnează o copie:
func (c *Car) OwnerList() []string {
return slices.Clone(c.Owners)
}
5. Încă două limite ale trucului cu JSON. Câmpurile de tip interfață își pierd tipul concret, iar doi pointeri care indicau spre același obiect devin, după copiere, două obiecte separate.
6. Copiile superficiale și goroutine-urile. Date comune modificate din mai multe goroutine produc data race-uri. Rulează testele cu go test -race: race detector-ul e făcut exact pentru asta.
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.