> serie: aprende go de 0 a 100 | artículo 10 de 19
La palabra puntero provoca sudores fríos a quien tocó C en la carrera y desconcierto a quien viene de Python o JS, donde todo es referencia sin que nadie lo diga. Los punteros en Go son la versión domesticada: el poder de elegir, sin aritmética ni segfaults por deporte.
La mecánica: & y *
Dos operadores y ya (A Tour of Go, pointers y especificación):
x := 42
p := &x // & obtiene la dirección: p es *int
fmt.Println(*p) // * desreferencia: 42
*p = 21 // modifica x a través de p
fmt.Println(x) // 21
// Lo que NO hay: aritmética de punteros.
// p++ no compila. Aquí no se pasea por la memoria.
El zero value de un puntero es nil, y desreferenciar un nil es panic instantáneo. Y a diferencia de C, no hay aritmética de punteros (FAQ): el recolector de basura te cubre las espaldas y el compilador te quita las ganas de hacer el ninja.
Todo se pasa por valor. Todo.
Aquí está la clave que ordena de una vez los punteros en Go: en Go TODO se pasa por copia. La pregunta es qué se copia. Con un struct, se copia el struct entero. Con un puntero, se copia la dirección (8 bytes) y ambas copias miran al mismo sitio:
type Usuario struct {
Nombre string
Visitas int
}
// Copia: el llamante no ve el cambio.
func visitaMal(u Usuario) {
u.Visitas++
}
// Puntero: modifica el original.
func visita(u *Usuario) {
u.Visitas++ // Go desreferencia solo: no hace falta (*u)
}
u := Usuario{Nombre: "Jenniffer"}
visita(&u)
fmt.Println(u.Visitas) // 1
Bonus mental para el artículo 8: slices y maps parecen pasarse por referencia porque su cabecera contiene un puntero interno. Se copia la cabecera, no los datos. Ahora ya cuadra todo, ¿verdad?

Cuándo usar puntero (y cuándo no)
La chuleta, destilada de las convenciones de la comunidad y de Effective Go:
- Puntero cuando la función o método debe MODIFICAR el receptor o argumento.
- Puntero cuando el struct es grande y copiarlo en cada llamada duele (medir antes de optimizar, eso sí).
- Puntero cuando necesitas distinguir «sin valor» (nil) de «valor cero».
- Valor para tipos pequeños e inmutables: números, strings, structs chicos de lectura.
- Coherencia: si un método del tipo usa receptor puntero, que lo usen todos.
Y una liberación para ex programadores de C:
// Dos formas de obtener un puntero a struct nuevo:
p1 := new(Usuario) // zero values
p2 := &Usuario{ // literal, la más común
Nombre: "Ada",
}
// Devolver punteros a variables locales es SEGURO:
// el compilador la mueve al heap si hace falta.
func crea() *Usuario {
u := Usuario{Nombre: "Grace"}
return &u // en C esto era pecado. Aquí no.
}
Devolver la dirección de una variable local es idiomático y seguro. El análisis de escape del compilador decide si esa variable vive en la pila o se muda al heap (Effective Go, allocation). Tú escribe código claro, que de la memoria se ocupa él.
Reto de la semana: pon a prueba los punteros en Go
- Escribe intercambia(a, b *int) que cruce los valores de dos variables y compruébalo desde main.
- Retoma la Cuenta del artículo 5 y verifica que Retira con receptor por valor no descuenta nada, y con receptor puntero sí.
- Crea func busca(id int) *Usuario que devuelva nil si no encuentra, y gestiona ese nil en el llamante sin provocar panic.
- Bonus: imprime &x en dos ejecuciones distintas del programa y observa que la dirección cambia. Las direcciones son detalles del runtime, no datos.
Punteros en Go: nivel completado.
En el próximo artículo empieza el plato fuerte: goroutines y el modelo CSP. La concurrencia por la que viniste a Go.
> exit