Declarar un método
Un método es una función con receptor: un parámetro extra escrito antes del nombre del método. El receptor ata el método a un tipo.
El nombre del receptor suele ser una o dos letras tomadas del tipo (c para Circle). Go no usa this ni self, y las guías de estilo desaconsejan esos nombres.
Como Circle tiene un método String() string, cumple fmt.Stringer, y fmt.Println imprime su resultado en lugar de {2}.
Receptores por valor y por puntero
Un receptor por valor (c Circle) recibe una copia. Un receptor puntero (c *Circle) recibe la dirección, así que los cambios en los campos se mantienen.
IncByValue compila y no hace nada visible, un bug clásico. Si un método debe modificar su receptor, necesita un receptor puntero.
Las llamadas también muestran las dos comodidades que te da Go. c.Inc() sobre una variable direccionable se reescribe como (&c).Inc(), y p.Value() sobre un puntero se reescribe como (*p).Value(). Rara vez escribes & o * solo para llamar a un método.
El & automático necesita algo direccionable. Un elemento de un map o un literal compuesto no lo son:
m := map[string]Counter{"a": {}}
m["a"].Inc() // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable
Guarda punteros en el map (map[string]*Counter) o saca el valor, cámbialo y vuelve a guardarlo.
Qué receptor elegir
| Usa un receptor puntero cuando | Usa un receptor por valor cuando |
|---|---|
| El método modifica el receptor | El tipo es pequeño y sus métodos nunca lo modifican (time.Time, un punto) |
| El struct es grande (copiarlo cuesta) | El tipo es un map, una func o un chan (ya es una referencia) |
El tipo contiene un sync.Mutex o similar | Quieres que el valor se comporte como un primitivo, seguro de copiar |
| Los otros métodos del tipo ya usan punteros |
Las FAQ de Go y la librería estándar siguen una regla práctica: sé coherente. Si algún método necesita un receptor puntero, dale receptor puntero a todos, para que el conjunto de métodos sea predecible. go vet marca un struct que contiene un sync.Mutex y se copia a través de un receptor por valor (passes lock by value).
Conjuntos de métodos e interfaces
La distinción importa sobre todo con las interfaces. El conjunto de métodos de T contiene los métodos con receptor por valor. El conjunto de métodos de *T contiene los métodos con receptor por valor y los de receptor puntero.
Escribir Square{Side: 3} sin el & en ese slice no compila:
cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)
El motivo: una interfaz puede contener una copia de un Square que no es direccionable, así que Go no puede tomar su dirección para llamar a un método de puntero. Consulta interfaces para el resto de la historia.
Métodos sobre tipos que no son structs
Cualquier tipo con nombre declarado en tu paquete puede tener métodos, no solo los structs.
Los límites: el tipo tiene que estar definido en el mismo paquete que el método, y su tipo subyacente no puede ser un puntero ni una interfaz. No puedes añadir métodos a int, string ni time.Time desde tu propio paquete. Envolverlos en un tipo con nombre nuevo es la forma de hacerlo en Go. String() sobre un tipo entero es también la manera de dar a los enums una salida legible; consulta enums con iota.
Method values y method expressions
Un método ligado a un receptor concreto es un valor función:
Un method value con receptor por valor copia el receptor al crearse, y por eso f sigue diciendo Ana. Con un receptor puntero guardaría el puntero y vería los cambios posteriores. Los method values vienen bien para los callbacks: http.HandleFunc("/", srv.handleIndex).
Errores comunes
- Receptor por valor en un método que modifica. Compila y cambia en silencio una copia.
- Mezclar tipos de receptor. Deja poco claro si es
To*Tquien cumple una interfaz. Elige uno por tipo. - Llamar a un método de puntero sobre un puntero nil. Está permitido, y el método se ejecuta con un receptor nil. Solo provoca un panic cuando toca un campo. Algunos tipos (como una lista enlazada segura ante nil) lo usan a propósito; la mayoría no lo espera.
Preguntas frecuentes
¿Qué diferencia hay entre un método y una función en Go?
Un método tiene un receptor, escrito entre func y el nombre: func (c Circle) Area() float64. Lo llamas sobre un valor (c.Area()), pertenece al conjunto de métodos del tipo y es la forma en que un tipo cumple interfaces. Por lo demás se comporta como una función cuyo primer argumento es el receptor.
¿Cuándo debo usar un receptor puntero en Go?
Usa un receptor puntero cuando el método tenga que modificar el receptor, cuando el struct sea lo bastante grande como para que copiarlo en cada llamada sea un desperdicio, o cuando el tipo contenga algo que no debe copiarse, como un sync.Mutex. Si algún método de un tipo necesita un receptor puntero, el consejo habitual es que todos lo sean, por coherencia.
¿Por qué mi tipo no implementa la interfaz cuando el método tiene un receptor puntero?
Los métodos con receptor puntero pertenecen al conjunto de métodos de *T, no al de T. Así que var s Shape = Square{} falla con Square does not implement Shape (method Area has pointer receiver). Asigna un puntero: var s Shape = &Square{}.
¿Se pueden definir métodos sobre tipos integrados en Go?
No directamente. Los métodos solo se pueden declarar sobre tipos con nombre definidos en el mismo paquete. Define primero tu propio tipo, type Celsius float64, y luego añádele métodos. No puedes añadir métodos a int, a string ni a un tipo de otro paquete.