Métodos en Go: value receivers vs pointer receivers

En Go no hay métodos dentro de la definición del struct. Los métodos se declaran fuera, ligados al tipo mediante un receiver. La decisión entre value receiver y pointer receiver no es solo una cuestión de estilo: determina si el método puede modificar el objeto original y qué interfaces puede satisfacer tu tipo.

Value receiver: trabaja con una copia

package main

import "fmt"

type Rectangulo struct {
    Ancho, Alto float64
}

// value receiver: recibe una copia del struct
func (r Rectangulo) Area() float64 {
    return r.Ancho * r.Alto
}

func (r Rectangulo) Perimetro() float64 {
    return 2 * (r.Ancho + r.Alto)
}

func main() {
    rect := Rectangulo{Ancho: 5, Alto: 3}
    fmt.Println(rect.Area())      // 15
    fmt.Println(rect.Perimetro()) // 16
}

El value receiver es correcto para métodos que no modifican el estado. Como recibe una copia, es seguro llamarlo sobre un valor o un puntero: Go hace la conversión automáticamente.

Pointer receiver: trabaja con el original

type Contador struct {
    valor int
}

// pointer receiver: puede modificar el struct
func (c *Contador) Incrementar() {
    c.valor++
}

func (c *Contador) Valor() int {
    return c.valor
}

func main() {
    c := Contador{}
    c.Incrementar()
    c.Incrementar()
    fmt.Println(c.Valor()) // 2
}

Cuándo usar cada uno

Usa pointer receiver cuando el método necesita modificar el struct, cuando el struct sea grande y copiar tenga coste, o cuando quieras consistencia (si un método usa pointer receiver, conviene que todos los demás también lo hagan). Usa value receiver para structs pequeños de solo lectura, tipos inmutables o cuando el método no necesita identidad de instancia.

El error de llamar un pointer receiver sobre un valor no direccionable

type Temp struct{ v int }

func (t *Temp) Doble() { t.v *= 2 }

func devuelveTemp() Temp { return Temp{5} }

func main() {
    // devuelveTemp().Doble() // ERROR: cannot take the address of devuelveTemp()
    t := devuelveTemp()
    t.Doble()               // OK: t es una variable direccionable
    fmt.Println(t.v)        // 10
}

El compilador te lo indica claramente:

cannot call pointer method on Temp
cannot take the address of devuelveTemp()

Implicaciones con interfaces

Un tipo solo satisface una interfaz si tiene todos los métodos del tipo que el compilador puede usar. Con un pointer receiver, solo *T implementa la interfaz; con value receiver, tanto T como *T la implementan:

type Describer interface {
    Describir() string
}

type Coche struct{ Marca string }

func (c *Coche) Describir() string {
    return "Coche: " + c.Marca
}

func main() {
    var d Describer

    // d = Coche{"Ford"}   // ERROR: Coche no implementa Describer
    d = &Coche{"Ford"}    // OK: *Coche sí implementa Describer
    fmt.Println(d.Describir())
}

Consejo práctico

Si tienes dudas, empieza con pointer receiver para todos los métodos de un tipo. El compilador te avisará si haces algo imposible y la penalización de rendimiento de no copiar suele ser positiva, no negativa. Cambiar de pointer a value receiver más adelante es un cambio de API silencioso; al revés puede romper código existente.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP