CAP 05 · LEC 05·Composición de datos

Copias vs referencias: cómo viaja la memoria en Go

En Go la asignación copia: arrays, structs e ints se duplican byte a byte. Pero slices, maps y channels son reference-like: copias el header, no el contenido. Entender esta diferencia es esencial para evitar mutaciones inesperadas.

● INTERMEDIO7 min lecturapor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

La asignación copia siempre

Cuando asignas un valor a otra variable o lo pasas a una función, Go copia los bytes. Para tipos básicos (int, bool, float64) y para structs, esto significa una copia completa e independiente.

package main import "fmt" type Point struct { X, Y int } func main() { // int — copia por valor a := 10 b := a b = 99 fmt.Println(a, b) // 10 99 // struct — copia byte a byte p1 := Point{X: 1, Y: 2} p2 := p1 p2.X = 999 fmt.Println(p1) // {1 2} — sin cambios fmt.Println(p2) // {999 2} // array — copia completa (no es como en C) arr1 := [3]int{1, 2, 3} arr2 := arr1 arr2[0] = 999 fmt.Println(arr1) // [1 2 3] fmt.Println(arr2) // [999 2 3] }
Salida10 99 {1 2} {999 2} [1 2 3] [999 2 3]

Slices, maps y channels: reference-like

Slices, maps y channels son estructuras internas que contienen un puntero al backing data. Al copiarlas, copias el header (puntero + metadata), pero el backing es compartido. Mutar el contenido a través de una copia afecta al original.

package main import "fmt" func main() { // Slice: el header (ptr, len, cap) se copia, // pero el array subyacente se comparte s1 := []int{1, 2, 3} s2 := s1 s2[0] = 999 fmt.Println(s1) // [999 2 3] — ¡mutación visible! fmt.Println(s2) // [999 2 3] // Map: igualmente, dos variables apuntan al mismo backing m1 := map[string]int{"a": 1} m2 := m1 m2["a"] = 999 fmt.Println(m1) // map[a:999] fmt.Println(m2) // map[a:999] // Para AISLAR un slice, copia explícitamente independent := make([]int, len(s1)) copy(independent, s1) independent[0] = -1 fmt.Println(s1) // [999 2 3] fmt.Println(independent) // [-1 2 3] }
Salida[999 2 3] [999 2 3] map[a:999] map[a:999] [999 2 3] [-1 2 3]
¿Por qué se llaman 'reference-like'?

Go no tiene referencias en el sentido de C++. Slices, maps y channels son valores que contienen un puntero internamente. La copia del valor copia el puntero — por eso se comportan como referencias sin serlo.

Paso de argumentos a funciones

Las funciones reciben copias de sus argumentos. Para tipos por valor (struct, int, array), la función no puede mutar el original. Para slices y maps, sí puede mutar el contenido (pero no la cabecera).

package main import "fmt" type Counter struct{ Value int } // Por valor: recibe copia del struct func incByValue(c Counter) { c.Value++ // muta la copia local } // Por puntero: recibe la dirección — puede mutar el original func incByPointer(c *Counter) { c.Value++ } // Slice: la función recibe copia del header, // pero apunta al mismo array. Puede mutar el contenido. func zeroFirst(s []int) { s[0] = 0 } // Sin embargo, REASIGNAR el slice dentro no afecta fuera func reassign(s []int) { s = []int{999, 999, 999} _ = s } func main() { c := Counter{Value: 5} incByValue(&c.Value) // (truco) — pero veamos el caso real: incByValue(c) fmt.Println(c.Value) // 5 — sin cambio incByPointer(&c) fmt.Println(c.Value) // 6 nums := []int{1, 2, 3} zeroFirst(nums) fmt.Println(nums) // [0 2 3] — contenido mutado reassign(nums) fmt.Println(nums) // [0 2 3] — la reasignación local no afecta }
Salida5 6 [0 2 3] [0 2 3]

Trampa: append dentro de funciones

Como append puede crear un array nuevo cuando se llena la capacidad, una función que hace append y no devuelve el slice resultante puede o no ver sus cambios reflejados fuera. Es un bug clásico.

package main import "fmt" // Mala idea: la función no devuelve el slice — el caller // puede o no ver el append dependiendo de la capacidad func addLocal(s []int, v int) { s = append(s, v) // si crece, asigna nuevo array → caller no lo ve } // Forma correcta: devolver el slice func add(s []int, v int) []int { return append(s, v) } func main() { // Capacidad justa para 3 — el append fuerza realojo a := make([]int, 3, 3) a[0], a[1], a[2] = 1, 2, 3 addLocal(a, 99) fmt.Println(a) // [1 2 3] — el append local se perdió a = add(a, 99) fmt.Println(a) // [1 2 3 99] // Con capacidad sobrante puede engañarte b := make([]int, 3, 5) b[0], b[1], b[2] = 1, 2, 3 addLocal(b, 77) // escribe en b[3] dentro del backing compartido fmt.Println(b[:4]) // [1 2 3 77] — visible POR ACCIDENTE }
Salida[1 2 3] [1 2 3 99] [1 2 3 77]
Regla: devuelve siempre el slice tras append

Como append puede reasignar memoria, el patrón seguro es s = add(s, v). Las funciones que añaden a un slice deben devolverlo, igual que el append de stdlib.

Modelo mental rápido

package main import "fmt" // Cheat sheet en código: // // Tipo Copia al asignar Mutación visible fuera // -------------- --------------------- ------------------------- // int, bool valor completo no // array [N]T valor completo no // struct valor completo no // *T (puntero) copia el puntero sí (a través de *) // []T (slice) copia header sí (al backing array) // map[K]V copia header sí (al backing) // chan T copia header sí (al backing) // string copia header (inmut.) no (las strings son inmutables) func main() { type Big struct { Data [1000]int } // Para structs grandes, pasa puntero — evita copiar 1000 ints b := &Big{} b.Data[0] = 42 fmt.Println(b.Data[0]) // 42 }
Salida42
Cuándo usar punteros

Usa puntero cuando: (1) quieras que la función mute el original; (2) el struct sea grande y la copia importe; (3) compartas un mismo valor entre varios sitios y todos deban ver los cambios. Para todo lo demás, valor es lo idiomático y más simple.