Lección 6 de 8. Aquí se cierra el cabo suelto de la lección 4: por qué unos métodos llevan asterisco y otros no. La regla de fondo es una sola y es sorprendentemente simple: en Go siempre se copia. Lo que cambia es qué se está copiando.
Qué es un puntero
Dos operadores y ya está. &x da la dirección de x; *p da el valor que hay en esa dirección.
x := 10
p := &x
fmt.Println("x =", x, " p apunta a", *p)
*p = 20
fmt.Println("tras *p = 20, x =", x)
salidax = 10 p apunta a 10 tras *p = 20, x = 20
nil.Siempre se copia
Un struct se copia entero cuando lo pasas a una función, así que la función trabaja sobre otra cosa:
func subirCopia(p Producto) { p.Precio += 10 }
func subirOriginal(p *Producto) { p.Precio += 10 }
a := Producto{Nombre: "Camiseta", Precio: 20}
subirCopia(a)
fmt.Printf("tras subirCopia: %.2f\n", a.Precio)
subirOriginal(&a)
fmt.Printf("tras subirOriginal: %.2f\n", a.Precio)
salidatras subirCopia: 20.00 tras subirOriginal: 30.00
Es el mismo caso del contador de la lección 4, ahora con el porqué delante: subirCopia recibió una copia del struct y la modificó; esa copia murió al salir de la función.
Pero los slices y los maps se comportan distinto
Y no es una excepción a la regla, es la regla aplicada a algo distinto. Un slice no es el array: es una ficha pequeña que apunta al array. Al copiar la ficha, el array sigue siendo el mismo.
func vaciarSlice(s []int) { s[0] = 99 }
func anadirSlice(s []int) []int { return append(s, 4) }
s := []int{1, 2, 3}
vaciarSlice(s)
fmt.Println("tras vaciarSlice:", s)
anadirSlice(s)
fmt.Println("tras anadirSlice (sin recoger):", s)
s = anadirSlice(s)
fmt.Println("tras recoger el resultado: ", s)
salidatras vaciarSlice: [99 2 3] tras anadirSlice (sin recoger): [99 2 3] tras recoger el resultado: [99 2 3 4]
Tres resultados que parecen contradictorios y no lo son. Modificar un elemento sí se ve fuera, porque el array es compartido. Añadir no, porque append puede necesitar un array más grande y mudarse a otro sitio; el slice de fuera se queda apuntando al viejo. Por eso append se reasigna siempre.
Con los maps no hay matices: siempre se ve fuera.
func tocarMapa(m map[string]int) { m["nuevo"] = 1 }
m := map[string]int{"a": 1}
tocarMapa(m)
fmt.Println(m)
salidamap[a:1 nuevo:1]
Crear algo ya apuntado
q := &Producto{Nombre: "Gorra", Precio: 9}
q.Precio = 12 // sin asterisco: Go lo pone por ti
fmt.Printf("%+v\n", *q)
n := new(int) // la otra forma, menos usada
fmt.Println("new(int) ->", *n)
salida{Nombre:Gorra Precio:12}
new(int) -> 0
Fíjate en q.Precio: aunque q es un puntero, no hace falta escribir (*q).Precio. Go desreferencia solo, y eso es la razón de que en la práctica los punteros casi no se noten.
El puntero nil
Un puntero sin asignar vale nil, y usarlo revienta el programa:
var p *Producto
fmt.Println("un puntero sin asignar vale:", p)
fmt.Println(p.Nombre)
se caeun puntero sin asignar vale: <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x498565]
Es el panic más común de Go. La forma de evitarlo es la de siempre: si una función puede devolver un puntero nulo, compruébalo antes de usarlo.
Entonces, ¿puntero o no?
| Situación | Qué hacer |
|---|---|
| La función o el método modifica el struct | Puntero, obligatorio |
| Solo lo lee y es pequeño | Por valor. Copiar unos campos sale barato |
| Solo lo lee pero es grande | Puntero, para no copiar |
| Puede «no haber nada» | Puntero, y comprobar nil |
| Es un slice o un map | Tal cual. Ya se comportan como quieres |
| Algunos métodos del tipo usan puntero | Que lo usen todos, por coherencia |
Todo el código de esta lección se compiló y ejecutó con Go 1.25 antes de publicarla. El panic del puntero nulo está provocado de verdad y el mensaje es el que imprimió el programa al caerse.