Cuando defines un método en Go, el primer parámetro (antes del nombre del método) es el receiver. Este receptor determina si el método opera sobre una copia de los datos o sobre la dirección de memoria original. Si declaras un value receiver (por ejemplo, func (r T)), Go crea una copia completa de la estructura en la pila cada vez que llamas al método; cualquier modificación dentro del método muere con la copia. Por el contrario, un pointer receiver (por ejemplo, func (r *T)) recibe la dirección de memoria, permitiéndote modificar el estado original y evitando la duplicación de datos pesados.
Usarás un value receiver cuando el tipo sea pequeño (como un time.Time o un int), cuando quieras garantizar la inmutabilidad de la estructura o cuando el método solo necesite leer datos sin alterarlos. El pointer receiver es obligatorio si el método debe modificar al receptor o si la estructura es lo suficientemente grande como para que copiarla constantemente impacte el rendimiento o sature el garbage collector. Sin embargo, existe una regla de oro de diseño: si un método de un tipo requiere un puntero, todos los demás métodos de ese tipo deberían usar punteros para mantener la consistencia del method set [disponible desde Go 1.0]. Si mezclas ambos de forma inconsistente, podrías encontrarte con errores de compilación al intentar llamar a métodos de puntero sobre variables que el compilador no puede garantizar que son “direccionables”.
El error crítico ocurre cuando intentas llamar a un método con pointer receiver sobre un valor que no tiene una dirección de memoria asignada, como un literal directo o un valor devuelto por una función.
package main
import (
"fmt"
"math"
)
// Rect representa un rectángulo simple.
type Rect struct {
Ancho float64
Alto float64
}
// Area es un value receiver. No modifica la estructura original.
// Es ideal para operaciones de solo lectura.
func (r Rect) Area() float64 {
return r.Ancho * r.Alto
}
// Scale es un pointer receiver. Modifica los campos de la instancia original.
func (r *Rect) Scale(factor float64) {
r.Ancho *= factor
r.Alto *= factor
}
// Duplicate es un value receiver que devuelve una nueva instancia.
// Se usa para seguir patrones de inmutabilidad.
func (r Rect) Duplicate() Rect {
return Rect{
Ancho: r.Ancho,
Alto: r.Alto,
}
}
func main() {
// Caso 1: Uso con variable (addressable)
r := Rect{Ancho: 10, Alto: 5}
// Llamada a value receiver: Area() lee r, no la cambia.
fmt.Printf("Área inicial: %.2f\n", r.Area())
// Llamada a pointer receiver: Scale() modifica r directamente.
// El compilador automáticamente toma la dirección de r (&r).
r.Scale(2)
fmt.Printf("Área tras escalar (puntero): %.2f\n", r.Area())
// Caso 2: Uso de inmutabilidad
r2 := r.Duplicate()
fmt.Printf("r2 (copia) después de escalar r: %.2f\n", r2.Area())
// Caso 3: Consistencia y tipos
// r.Scale(1) fallaría si r fuera un valor no direccionable,
// pero como es una variable declarada, Go lo gestiona.
fmt.Printf("Estado final de r: %+v\n", r)
}
Análisis del código
En el ejemplo, r.Area() utiliza un value receiver. Cuando se ejecuta, Go copia los campos Ancho y Alto a una nueva zona de memoria para la función; es seguro y eficiente para tipos pequeños.
r.Scale(2) utiliza un pointer receiver. Aunque la sintaxis parece una llamada normal, Go está operando sobre la dirección de memoria de r. Esto es lo que permite que r.Ancho y r.Alto cambien de valor permanentemente en la función main. Fíjate que no necesitamos escribir (&r).Scale(2); el compilador aplica automáticamente el dereferencing si la variable es direccionable.
La función Duplicate ejemplifica el patrón de inmutabilidad. Al usar un value receiver y devolver un nuevo Rect, garantizamos que el objeto original permanezca intacto, lo cual es una práctica común en sistemas concurrentes para evitar race conditions.
El error frecuente
Un error común es intentar llamar a un método de puntero sobre un literal. Por ejemplo:
// Esto fallará al compilar
Rect{Ancho: 10, Alto: 5}.Scale(2)
El error será: cannot call pointer method on Rect literal.
Esto sucede porque un literal es un valor temporal en el código, no una variable con una dirección de memoria persistente en la pila. El compilador no puede tomar la dirección de algo que no tiene una identidad de memoria estable. Para arreglarlo, debes asignar el valor a una variable primero, haciendo que el valor sea addressable (direccionable), como hicimos en el main con r := Rect{...}.
N° 69