Saltar al contenido principal

Go desde cero, parte 10: punteros y cuándo usarlos (sin flashbacks de C)

Curso: Go — Lección 12 de 17

> 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?

Punteros en Go: Diagrama pixelart ilustrando los punteros en Go: paso por valor con dos cajas independientes, frente a paso por puntero con una flecha compartida hacia una sola caja
[Valor copia, puntero comparte: la decisión que cambia el comportamiento.]

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
Retrato pixel art de Jenniffer Cubillos

gracias por leer