Los receptores en Go definen la semántica de paso de datos para los métodos asociados a un tipo, determinando si el método opera sobre una copia del valor o sobre una referencia a la dirección de memoria original. A diferencia de otros lenguajes orientados a objetos donde el puntero this o self suele estar predefinido, Go obliga al desarrollador a declarar explícitamente si el receptor es de valor (t T) o de puntero (t *T), permitiendo un control granular sobre la mutabilidad y la eficiencia en la gestión de memoria.
Esta distinción técnica responde a la filosofía de Go de evitar efectos secundarios ocultos. En lenguajes con paso por referencia implícito, la modificación de un objeto puede acarrear consecuencias inesperadas en otros ámbitos del programa. En Go, al usar un receptor de valor, el compilador garantiza la inmutabilidad del dato original dentro del ámbito del método, ya que cualquier cambio se aplica exclusivamente a una copia local situada en el stack. Por el contrario, los receptores de puntero resuelven la necesidad de persistir cambios en el estado del objeto y optimizan el rendimiento al evitar la copia de estructuras de gran tamaño.
Mecánica interna y Method Sets
La elección del receptor impacta directamente en el method set (conjunto de métodos) de un tipo. Un tipo de valor T tiene un method set compuesto únicamente por métodos con receptores de valor. Sin embargo, un tipo de puntero *T posee un method set que abarca tanto los métodos de valor como los de puntero. Esto se debe a que un puntero puede ser desreferenciado de forma segura para obtener el valor, pero un valor no siempre puede ser direccionado de forma segura si no reside en una variable direccionable.
package main
import "fmt"
type Contador struct {
valor int
}
// Receptor de valor: opera sobre una copia. Inmutabilidad garantizada.
func (c Contador) IncrementarCopia() {
c.valor++
} // El cambio muere al salir del scope
// Receptor de puntero: opera sobre la dirección de memoria. Permite mutación.
func (c *Contador) IncrementarReal() {
c.valor++
}
func main() {
c := Contador{valor: 10}
c.IncrementarCopia()
fmt.Println(c.valor) // -> 10
c.IncrementarReal()
fmt.Println(c.valor) // -> 11
// Go permite llamar a métodos de puntero desde valores direccionables
(&c).IncrementarReal()
fmt.Println(c.valor) // -> 12
}
GoEl comportamiento más contraintuitivo ocurre cuando se intenta satisfacer una interfaz: si un método está definido con un receptor de puntero, solo el puntero al tipo implementará dicha interfaz, mientras que el valor por sí solo fallará en tiempo de compilación.
La decisión de diseño debe priorizar la consistencia del tipo. Si un solo método del tipo requiere un receptor de puntero para modificar el estado, es una convención aceptada en la comunidad de Go que todos los demás métodos del mismo tipo también utilicen receptores de puntero, incluso aquellos que solo realizan operaciones de lectura. Esto evita confusiones sobre la semántica del tipo y previene errores sutiles al utilizar el tipo detrás de una interfaz o en operaciones de concurrencia.
Indirección automática y limitaciones de direccionabilidad
El compilador de Go realiza una optimización conocida como indirección automática para facilitar la ergonomía del código. Cuando se invoca un método con receptor de puntero sobre una variable que es un valor, Go interpreta internamente valor.Metodo() como (&valor).Metodo(). No obstante, esto solo es posible si el valor es direccionable (addressable), lo que excluye a elementos de un mapa o valores de retorno inmediatos de una función.
type Perfil struct {
Nombre string
}
func (p *Perfil) SetNombre(n string) {
p.Nombre = n
}
func obtenerPerfil() Perfil {
return Perfil{Nombre: "Kaelo"}
}
func ejemploIndireccion() {
p := Perfil{Nombre: "Felipe"}
p.SetNombre("Kaelo") // Válido: p es direccionable, Go hace (&p).SetNombre
// obtenerPerfil().SetNombre("Nuevo")
// Error: cannot call pointer method on obtenerPerfil() (non-addressable)
}
GoUn edge case crítico surge en el uso de mapas: los elementos de un mapa en Go no son direccionables por razones de seguridad de memoria, ya que el crecimiento del mapa puede reubicar los elementos en nuevas direcciones. Por lo tanto, es imposible llamar a un método con receptor de puntero directamente sobre un elemento de un mapa de valores map[string]T, obligando al uso de punteros en el mapa map[string]*T si se requiere mutabilidad.
- Módulo: Métodos e Interfaces
- Artículo número: #80