En esta página

Punteros

14 min lectura TextoCap. 3 — Datos y errores

Punteros en Go: seguridad sin complejidad

Un puntero es una variable que almacena la dirección de memoria de otro valor. Si x contiene el valor 42 en la posición de memoria 0xc00001e070, entonces un puntero a x contiene el valor 0xc00001e070.

Los punteros son esenciales en muchos lenguajes de bajo nivel, pero son notoriamente difíciles de manejar correctamente en C o C++. Go toma un término medio: incluye punteros para darles control y eficiencia, pero los hace seguros al eliminar la aritmética de punteros.

Los operadores `&` y `*`

Go usa dos operadores fundamentales para trabajar con punteros:

  • & (dirección-de): retorna la dirección de memoria de una variable
  • * (desreferencia): accede al valor al que apunta un puntero
x := 42         // variable normal de tipo int
p := &x         // p es de tipo *int — "puntero a int"
                 // p contiene la dirección de memoria de x

fmt.Println(x)   // 42
fmt.Println(p)   // 0xc00001e070 (alguna dirección)
fmt.Println(*p)  // 42 — el valor al que apunta p

*p = 99          // modificar el valor a través del puntero
fmt.Println(x)   // 99 — x fue modificada

Tipos puntero

El tipo de un puntero es *T donde T es el tipo al que apunta:

var pi *int         // puntero a int (nil por defecto)
var ps *string      // puntero a string (nil por defecto)
var pf *float64     // puntero a float64 (nil por defecto)

n := 42
pi = &n
fmt.Println(*pi)  // 42

Paso por valor vs. paso por referencia

Esta es la distinción más importante en el manejo de punteros:

// PASO POR VALOR — la función recibe una copia completa
func doblarValor(n int) int {
    n *= 2
    return n  // retorna la copia modificada
}

// PASO POR REFERENCIA (usando puntero)
func doblarInPlace(n *int) {
    *n *= 2  // modifica el valor original
}

func main() {
    a := 5

    resultado := doblarValor(a)
    fmt.Println(a, resultado)  // 5, 10 — a sin cambio

    doblarInPlace(&a)
    fmt.Println(a)  // 10 — a modificada
}

Structs: valor vs. puntero

type Punto struct{ X, Y float64 }

// Paso por valor: la función recibe una copia del struct completo
func moverCopia(p Punto, dx, dy float64) Punto {
    p.X += dx
    p.Y += dy
    return p
}

// Paso por puntero: más eficiente y modifica el original
func moverOriginal(p *Punto, dx, dy float64) {
    p.X += dx
    p.Y += dy
}

func main() {
    p := Punto{X: 0, Y: 0}

    p2 := moverCopia(p, 5, 3)
    fmt.Println(p)  // {0 0} — sin cambio
    fmt.Println(p2) // {5 3}

    moverOriginal(&p, 5, 3)
    fmt.Println(p)  // {5 3} — modificado
}

Cuándo usar punteros

La regla general en Go:

Usa puntero cuando:

  1. El método necesita modificar el receptor
  2. El struct es grande (evitar copias costosas — más de ~64 bytes)
  3. Necesitas representar un valor opcional (nil como "sin valor")
  4. Consistencia: si algunos métodos del tipo necesitan puntero, hazlos todos puntero

Usa valor cuando:

  1. El tipo es pequeño (int, string, struct de 1-2 campos)
  2. El tipo no debe modificarse (tipos inmutables)
  3. Tipos básicos: siempre por valor (int, string, bool)
// Ejemplos reales de cuándo usar puntero

// 1. Método que modifica
func (c *Contador) Incrementar() { c.valor++ }

// 2. Struct grande por puntero
type Imagen struct {
    Pixels [1920][1080][3]uint8  // ~6MB — nunca copiar
}
func procesarImagen(img *Imagen) { /* ... */ }

// 3. Valor opcional
func buscarUsuario(id int) *Usuario {
    // retorna nil si no se encuentra
    if id == 0 {
        return nil
    }
    return &Usuario{ID: id, Nombre: "Ana"}
}
usuario := buscarUsuario(1)
if usuario != nil {
    fmt.Println(usuario.Nombre)
}

La función `new()`

new(T) asigna memoria para un valor de tipo T, inicializa con el zero value y retorna un puntero *T:

p := new(int)       // *int que apunta a 0
*p = 42
fmt.Println(*p)     // 42

s := new(string)    // *string que apunta a ""
*s = "hola"

cfg := new(Config)  // *Config con todos los campos en zero value
cfg.Host = "localhost"

// Equivalente más idiomático usando literal con &
cfg2 := &Config{Host: "localhost", Puerto: 8080}

En la práctica, new se usa raramente. La forma más idiomática es &T{} o &T{campo: valor}.

Desreferencia automática en structs

Go desreferencia punteros automáticamente cuando accedes a campos de structs:

type Config struct {
    Host   string
    Puerto int
}

cfg := &Config{Host: "localhost", Puerto: 8080}

// Estos dos son equivalentes
fmt.Println((*cfg).Host)  // desreferencia explícita (tedioso)
fmt.Println(cfg.Host)      // Go lo hace automáticamente (idiomático)

// También funciona al llamar métodos
cfg.Activar()  // equivale a (*cfg).Activar()

Nil pointers: el error más común

El error más frecuente con punteros en Go es desreferenciar un puntero nil:

var p *int
fmt.Println(p)   // <nil>
fmt.Println(*p)  // ¡PANIC: runtime error: invalid memory address or nil pointer dereference!

// Siempre verifica antes de desreferenciar
if p != nil {
    fmt.Println(*p)
}

Patron seguro con punteros opcionales

type Usuario struct {
    Nombre  string
    Apellido string
    Email   *string  // nil = no proporcionado
}

u := Usuario{
    Nombre:   "Ana",
    Apellido: "García",
}

// Verificar antes de acceder
if u.Email != nil {
    fmt.Println("Email:", *u.Email)
} else {
    fmt.Println("Email no proporcionado")
}

// Asignar un puntero a string
email := "[email protected]"
u.Email = &email

Punteros y goroutines: cuidado con las carreras de datos

Cuando múltiples goroutines acceden al mismo dato a través de punteros sin sincronización, pueden ocurrir carreras de datos:

// INCORRECTO: carrera de datos
contador := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        contador++  // carrera de datos: múltiples goroutines leen y escriben
    }()
}
wg.Wait()

// CORRECTO: usar sync.Mutex o sync/atomic
import "sync/atomic"

var contador int64
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        atomic.AddInt64(&contador, 1)  // operación atómica
    }()
}
wg.Wait()

Sin aritmética de punteros

Go elimina deliberadamente la aritmética de punteros que existe en C:

// En C: p++ avanza el puntero al siguiente elemento
// En Go: esto no existe — usa slices en su lugar

nums := []int{10, 20, 30, 40, 50}

// En lugar de aritmética de punteros, usa indexación de slice
for i := range nums {
    fmt.Println(nums[i])
}

// O range
for _, n := range nums {
    fmt.Println(n)
}

Esta restricción hace que el código Go sea libre de vulnerabilidades clásicas como buffer overflows y use-after-free que son comunes en C/C++.

Con los punteros dominados, en la siguiente lección aprenderemos el manejo de errores en Go — la forma explícita, robusta e idiomática de manejar fallos sin excepciones.

Desreferenciar un puntero nil causa pánico
Si intentas acceder al valor de un puntero nil (con *p o p.Campo), Go lanzará un pánico en tiempo de ejecución. Siempre verifica if p != nil antes de desreferenciar punteros que podrían ser nil, especialmente los que provienen de funciones o parámetros.
Go no tiene aritmética de punteros
A diferencia de C o C++, no puedes hacer p++ o p+1 para avanzar un puntero a la siguiente posición de memoria. Esta restricción elimina toda una categoría de vulnerabilidades de seguridad (buffer overflows, use-after-free). Si necesitas indexar memoria, usa slices.