En esta página
Punteros
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 modificadaTipos 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) // 42Paso 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:
- El método necesita modificar el receptor
- El struct es grande (evitar copias costosas — más de ~64 bytes)
- Necesitas representar un valor opcional (nil como "sin valor")
- Consistencia: si algunos métodos del tipo necesitan puntero, hazlos todos puntero
Usa valor cuando:
- El tipo es pequeño (int, string, struct de 1-2 campos)
- El tipo no debe modificarse (tipos inmutables)
- 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 = &emailPunteros 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.
Inicia sesión para guardar tu progreso