El method set (o conjunto de métodos) de un tipo es la lista de todos los métodos que ese tipo puede ejecutar. En Go, la capacidad de un tipo para satisfacer una interfaz depende estrictamente de si sus métodos pertenecen al set de métodos del tipo en cuestión. Aquí es donde la distinción entre un tipo T y un puntero *T se vuelve fundamental: el set de métodos de T solo incluye los métodos con receptor por valor (value receiver), mientras que el set de métodos de *T incluye tanto los métodos con receptor por valor como los que tienen receptor por puntero (pointer receiver).
Esta distinción existe por una razón de seguridad y memoria: un método con receptor por puntero necesita la dirección de memoria para poder modificar el valor original o para evitar la copia de estructuras pesadas. Aunque Go es capaz de realizar una conversión automática de t.Method() a (&t).Method() cuando trabajas con variables directas (lo que conocemos como syntactic sugar), no puede hacer lo mismo cuando intentas asignar un valor a una interfaz. Para que un tipo satisfaga una interfaz, el compilador debe estar seguro de que el método es accesible sin necesidad de realizar conversiones implícitas que podrían fallar si el valor es una copia.
Usa receptores por puntero cuando el método deba mutar el estado del receptor o cuando el tipo sea una estructura grande y quieras evitar la copia en cada llamada. Si intentas implementar una interfaz usando únicamente receptores por puntero, solo los tipos puntero (*T) implementarán dicha interfaz. Si te equivocas en esta elección, el compilador rechazará tu código con un error de satisfacción de interfaz, impidiendo que pases valores de tu tipo a funciones que esperen la interfaz.
package main
import "fmt"
// Driver define una interfaz con un método para arrancar.
type Driver interface {
Start()
}
// Motor es un tipo que usa receptores por valor.
// Su set de métodos incluye Start() para Motor y para *Motor.
type Motor struct {
Name string
}
func (m Motor) Start() {
fmt.Printf("Motor %s: encendido (por valor)\n", m.Name)
}
// Sensor es un tipo que usa receptores por puntero.
// Su set de métodos SOLO incluye Start() para *Sensor.
type Sensor struct {
ID int
}
func (s *Sensor) Start() {
fmt.Printf("Sensor %d: activo (por puntero)\n", s.ID)
}
// EjecutarDriver acepta cualquier tipo que implemente Driver.
func EjecutarDriver(d Driver) {
d.Start()
}
func main() {
// Caso 1: Motor (Value Receiver)
// Cumple la interfaz tanto como valor como puntero.
m := Motor{Name: "V8"}
EjecutarDriver(m) // OK: m es Motor
EjecutarDriver(&m) // OK: *Motor tiene el método de Motor
// Caso 2: Sensor (Pointer Receiver)
// Solo el puntero cumple la interfaz.
s := Sensor{ID: 42}
// EjecutarDriver(&s) // OK: s es un puntero a Sensor
EjecutarDriver(&s)
// La siguiente línea fallaría al compilar:
// EjecutarDriver(s)
// error: Sensor does not implement Driver (Start method has pointer receiver)
fmt.Println("Ejecución completada con éxito.")
}
Análisis del comportamiento
En el ejemplo, observa cómo Motor se comporta de manera permisiva. Como su método Start tiene un receptor por valor, el compilador considera que tanto Motor como *Motor poseen el método Start en su set de métodos. Por eso, EjecutarDriver(m) y EjecutarDriver(&m) son ambos válidos. El runtime puede tratar a m como un valor o tomar su dirección de forma segura.
Por el contrario, Sensor es más restrictivo. Al definir Start con un receptor por puntero (s *Sensor), el método se añade al set de métodos de *Sensor, pero no al de Sensor. Esto ocurre porque el compilador no puede garantizar que llamar a s.Start() sobre un valor Sensor sea seguro si el método requiere una dirección de memoria para operar, ya que s podría ser una copia temporal de la estructura.
Cuando llamas a EjecutarDriver(&s), estás pasando un tipo puntero que tiene en su set de métodos la firma necesaria para satisfacer la interfaz Driver. Si intentaras pasar s (el valor), el compilador revisaría el set de métodos de Sensor, vería que Start no está ahí (porque solo está en el set de *Sensor) y abortaría la compilación.
El error frecuente
El error más común ocurre al intentar pasar un valor a una función que espera una interfaz cuando los métodos han sido definidos con receptores por puntero:
type Updater interface {
Update(val int)
}
type Config struct {
Version int
}
func (c *Config) Update(val int) {
c.Version = val
}
func Apply(u Updater) {
u.Update(10)
}
func main() {
cfg := Config{Version: 1}
// Error de compilación:
// Apply(cfg)
}
En este escenario, aunque cfg es una variable válida, cfg (de tipo Config) no implementa Updater. Solo &cfg (de tipo *Config) lo hace. El error suele ser sutil porque, si usas cfg.Update(10) directamente, Go te permite hacerlo mediante una conversión implícita de la dirección, pero esa “magia” no se extiende a la comprobación de interfaces.
Al diseñar tus tipos, decide si el objetivo es la mutabilidad o la inmutabilidad, y mantén esa consistencia para evitar errores de satisfacción de interfaces.
N° 70