Punteros y valores en Go

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
Y aquí acaba todo lo complicado. Go no tiene aritmética de punteros: no puedes sumarle 1 a un puntero para moverte por la memoria, como en C. Un puntero en Go solo sirve para dos cosas: decir «quiero el original, no una copia» y representar «aquí puede no haber nada» con nil.

Siempre se copia

Qué se copia al pasar un struct, un slice o un map a una funciónQué se copia cuando pasas algo a una funciónen Go SIEMPRE se copia el valor; lo que cambia es qué es ese valorstructse copia enterola función toca su copia;el original no se enteraslicese copia la fichapero la ficha apunta al mismoarray: s[0]=9 sí se ve fueramapse copia la referencialos dos nombres son el mismomapa: siempre se ve fueraLa excepción del slice: append puede mudarlo a otro array, y entonces el de fuera se queda como estabaPor eso append SIEMPRE se reasigna: s = append(s, x)Y la regla corta: si la función tiene que modificar un struct, pásale un puntero con &Si solo lo lee y es pequeño, pásalo tal cual: copiar sale barato

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.