Punteros en Go: direcciones, desreferencia y seguridad

Un puntero en Go es una variable que almacena la dirección de memoria de otra variable. En lugar de contener un valor directo (como un int o un string), contiene la ubicación en la que ese valor reside. Utilizas el operador & (address-of) para obtener la dirección de una variable y el operador * (dereference) para acceder al valor contenido en esa dirección.

A diferencia de lenguajes como C, en Go no existe la aritmética de punteros (no puedes hacer p++ para moverte a la siguiente posición de memoria de forma segura). Esta es una decisión de diseño fundamental para garantizar la seguridad de tipos y permitir que el Garbage Collector (GC) funcione de manera eficiente. Si pudieras manipular direcciones de memoria arbitrariamente, el GC perdería el rastro de qué memoria pertenece a qué objeto, dificultando la gestión automática de la memoria y permitiendo accesos fuera de los límites de los objetos (out-of-bounds), lo que causaría corrupciones de memoria críticas.

Usarás punteros principalmente cuando necesites modificar el estado de un objeto dentro de una función (paso por referencia) o cuando quieras evitar la copia de estructuras grandes para optimizar el rendimiento. Sin embargo, si manejas punteros incorrectamente, especialmente trabajando con valores nulos, podrías provocar un pánico en tiempo de ejecución (nil pointer dereference).

package main

import (
	"fmt"
)

// Producto representa un artículo en nuestro inventario.
type Producto struct {
	Nombre string
	Precio float64
	Stock  int
}

// updateStock recibe un puntero a Producto para modificar su estado original.
func updateStock(p *Producto, cambio int) {
	if p == nil {
		return
	}
	// Go realiza un "dereferencing" implícito aquí.
	// Escribir p.Stock es un azúcar sintáctico de (*p).Stock
	p.Stock += cambio
}

func main() {
	// 1. Uso de literal de struct con el operador de dirección (&).
	// Esta es la forma más idiomática en Go para inicializar y obtener un puntero.
	p1 := &Producto{
		Nombre: "Teclado Mecánico",
		Precio: 149.50,
		Stock:  25,
	}

	// 2. Uso de new(T).
	// new(Producto) reserva memoria para un Producto, la inicializa con 
	// valores "zero-valued" (0, "", 0.0) y retorna un puntero (*Producto).
	p2 := new(Producto)
	p2.Nombre = "Ratón Óptico" // Acceso directo mediante el puntero

	// 3. Modificación mediante punteros
	updateStock(p1, -5)

	fmt.Printf("Producto 1 (modificado): %+v\n", p1)
	fmt.Printf("Producto 2 (inicializado con new): %+v\n", p2)

	// 4. Operador de desreferencia (*)
	// Usamos *p1 para obtener el valor real al que apunta la dirección de p1.
	fmt.Printf("Valor actual de stock de p1: %d\n", *p1.Stock) // Error de concepto común: p1.Stock ya es el valor.
	// Corrección técnica:
	fmt.Printf("Copia del valor de p1: %+v\n", *p1)
	fmt.Printf("Dirección de memoria de p1: %p\n", p1)
}

Desglose del ejemplo

En el código anterior, observa cómo manejamos la memoria de forma controlada. Cuando definimos p1 := &Producto{...}, estamos creando una instancia de Producto y obteniendo su dirección de memoria inmediatamente. Esto es preferible a crear la variable y luego usar & en una línea separada.

La función updateStock es clave: recibe p *Producto. Si hubiéramos pasado p Producto (sin el asterisco), Go habría creado una copia completa de la estructura en la pila (stack). Cualquier cambio dentro de la función se perdería al retornar. Al usar el puntero, modificamos la memoria original. Nota que dentro de updateStock, al hacer p.Stock, no escribimos (*p).Stock. Go realiza el desreferenciamiento automáticamente por ti cuando accedes a un campo a través de un puntero, lo que hace el código mucho más limpio.

Respecto a new(Producto), este es útil cuando solo te interesa tener la variable en memoria con sus valores por defecto (zero-valued) y quieres trabajar con su dirección. Sin embargo, en la práctica, verás mucho más el uso de &Producto{} porque permite la inicialización directa de campos, algo que new no permite de forma atómica.

El error frecuente

El error más común con punteros es el intento de desreferenciar un puntero nil.

// error_example.go
func main() {
    var p *Producto // p es nil, no apunta a nada
    p.Nombre = "Error" // ¡PANIC: runtime error: invalid memory address or nil pointer dereference
}

Cuando declaras un puntero sin asignarle una dirección (ya sea mediante new, un literal o la dirección de otra variable), su valor es nil. Intentar acceder a sus campos o desreferenciarlo con *p provocará que el programa termine abruptamente. Siempre verifica si un puntero es nil antes de operar con él si existe la posibilidad de que sea nulo.

64

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio