punteros-go-referencias-desreferencias

Punteros en Go: referencias, desreferencias y nil

  • 4 min

Un puntero en Go es un valor que guarda la dirección de memoria de otro valor. Sirve para compartir o modificar datos sin copiarlos enteros.

Si vienes de lenguajes como JavaScript o Python, la palabra “Puntero” puede sonar a bajo nivel, direcciones raras y cosas que normalmente no quieres mirar de cerca. Si vienes de C o C++, probablemente tengas pesadillas con segmentation faults y aritmética de memoria.

Tengo una buena noticia: Los punteros en Go son diferentes.. Go ha cogido la potencia de los punteros de C (la capacidad de acceder a memoria directamente) pero ha eliminado la parte peligrosa (la aritmética de punteros).

El resultado es un sistema que nos permite escribir código muy eficiente sin dispararnos en el pie.

Vamos a entender qué son, para qué sirven y cómo usar los operadores & y *.

Qué es exactamente un puntero

Imagina que una variable es una caja donde guardas un dato (por ejemplo, el número 42). Esa caja está guardada en una estantería gigante (la memoria RAM) y tiene una etiqueta con su ubicación (por ejemplo, Estantería 0x001).

  • Una Variable es la caja con el valor (42).
  • Un Puntero es un trozo de papel donde tienes apuntada la dirección de la estantería (0x001).

Si tienes el papel (el puntero), puedes ir a la estantería y ver lo que hay dentro, o incluso cambiarlo.

En términos técnicos, un puntero es un valor que contiene la dirección de memoria de otro valor.

Los operadores & y *

Para trabajar con punteros necesitamos distinguir dos operadores.

El operador &

Se lee como “dirección de”. Puesto delante de una variable, nos dice dónde está guardada en memoria.

numero := 10
direccion := &numero // "Dame la dirección de memoria de 'numero'"

fmt.Println(numero)    // Imprime: 10
fmt.Println(direccion) // Imprime: 0xc0000140b0 (Una dirección hexadecimal)
Copied!

El tipo de la variable direccion es *int (puntero a entero).

El operador *

Se lee como “valor en” (o desreferencia). Puesto delante de un puntero, viaja hasta esa dirección de memoria y nos da acceso al contenido.

// Siguiendo el ejemplo anterior...
valor := *direccion // "Ve a 0xc0000140b0 y dime qué hay allí"

fmt.Println(valor) // Imprime: 10

// ¡Podemos modificarlo!
*direccion = 20 // "Ve a esa dirección y pon un 20"
fmt.Println(numero) // Imprime: 20 (¡La variable original ha cambiado!)
Copied!

Regla mnemotécnica:

  • & (Address): ¿DÓNDE estás?
  • * (Value): ¿QUÉ tienes dentro?

Cuándo usar punteros

Los dos motivos habituales son compartir una mutación y evitar determinadas copias.

Modificar variables dentro de funciones

Como vimos en el artículo anterior, Go pasa los argumentos por copia. Si quieres que una función cambie una variable original, tienes que pasarle un puntero.

// Recibe un puntero a int (*int)
func Duplicar(n *int) {
    *n = *n * 2 // Modificamos el valor EN la dirección
}

func main() {
    x := 5
    Duplicar(&x) // Pasamos la DIRECCIÓN de x
    fmt.Println(x) // Imprime: 10
}
Copied!

Evitar copias grandes

Si pasamos una estructura grande por valor, la semántica exige copiarla. Al pasar un puntero copiamos solo la dirección y podemos evitar esa copia de datos.

Esto no significa que un puntero siempre sea más rápido. Puede hacer que el dato escape al heap, añadir indirecciones y aumentar el trabajo del recolector de basura. Para estructuras pequeñas, el valor suele ser más sencillo y puede rendir mejor; en código crítico conviene medir.

Aritmética de punteros

En este punto Go se gana bastante bien la etiqueta de “seguro”.

En C, si tienes un puntero apuntando a una posición de memoria, puedes hacer puntero + 1 para acceder a la siguiente posición de memoria. Esto es potentísimo, pero si te equivocas, puedes leer memoria de otro programa o del sistema operativo, causando pantallas azules o vulnerabilidades de seguridad (Buffer Overflow).

En el Go habitual, la aritmética de punteros no está permitida.

ptr := &x
// ptr++ // ERROR DE COMPILACIÓN: non-numeric type *int
Copied!

El paquete unsafe permite operaciones de bajo nivel que se salen de estas garantías, pero está reservado para casos muy concretos. No lo necesitamos para el uso normal de punteros.

El valor nil

El Zero Value de un puntero es nil. Esto significa que no está apuntando a ninguna dirección válida.

El pánico del programador: Si intentas desreferenciar (*) un puntero que es nil, tu programa explotará (panic).

var p *int // p es nil
// fmt.Println(*p) // PANIC: invalid memory address or nil pointer dereference
Copied!

Siempre asegúrate de que un puntero no sea nil antes de usarlo o de que haya sido inicializado.