Un método es, esencialmente, una función que tiene un receiver (o receptor) definido antes del nombre de la función. Este receiver actúa como el primer argumento implícito de la función, permitiendo que un tipo específico “posea” ese comportamiento. Técnicamente, un método es solo azúcar sintáctica sobre una función normal donde el primer parámetro es el tipo sobre el que se opera.
Esta arquitectura se diseñó para permitir que los tipos tengan una identidad clara y comportamientos asociados, facilitando la implementación de interfaces (conjuntos de firmas de métodos que un tipo debe cumplir para ser considerado de un tipo específico). Debes usar métodos cuando la lógica de una función está intrínsecamente ligada al estado de un objeto o cuando necesitas que un tipo satisfaga una interfaz predefinida, como fmt.Stringer. Si intentas definir un método para un tipo que no has declarado en tu mismo package (como un int nativo o un time.Time de otro paquete), el compilador fallará. Esto es una decisión de diseño para evitar el “monkey patching” y garantizar que la lógica de un paquete sea inmutable desde fuera.
package main
import (
"fmt"
)
// Celsius es un tipo nombrado basado en float64.
// Al crear un tipo nuevo, podemos añadirle métodos.
type Celsius float64
// String implementa la interfaz fmt.Stringer para Celsius.
// El receiver 'c' es el valor sobre el cual se ejecuta el método.
func (c Celsius) String() string {
return fmt.Sprintf("%.2f°C", c)
}
// Sensor representa un dispositivo de medición.
type Sensor struct {
Nombre string
Valor Celsius
}
// Leer muestra el estado actual del sensor.
func (s Sensor) Leer() string {
return fmt.Sprintf("Sensor %s: %s", s.Nombre, s.Valor)
}
// LeerManual es una función normal que hace lo mismo que el método Leer.
// Sirve para ilustrar la equivalencia mecánica entre método y función.
func LeerManual(s Sensor) string {
return fmt.Sprintf("Sensor %s: %s", s.Nombre, s.Valor)
}
func main() {
s := Sensor{Nombre: "Termómetro de Cocina", Valor: 22.5}
// 1. Uso estándar: Method Value.
// 's' es el receiver implícito.
fmt.Println("Uso estándar:", s.Leer())
// 2. Method Expression: Equivale a usar el tipo y pasar la instancia.
// Esto es lo que sucede internamente.
fmt.Println("Method Expression:", Sensor.Leer(s))
// 3. Uso de la función manual para demostrar la equivalencia.
fmt.Println("Función manual:", LeerManual(s))
// 4. Los tipos nombrados (Celsius) pueden tener sus propios métodos.
// Aunque Celsius no es un struct, funciona igual.
fmt.Println("Valor del sensor:", s.Valor.String())
}
Desglose del ejemplo
En el código anterior, observa cómo Celsius no es un tipo nuevo desde el punto de vista de la memoria (sigue siendo un float64), pero al declararlo como type Celsius float64, le otorgamos la capacidad de tener métodos. El método String() usa el receiver c para acceder al valor numérico y formatearlo.
Cuando ejecutamos s.Leer(), estamos utilizando lo que se conoce como un method value. El compilador entiende que s debe ser el primer argumento de la función Leer. Por el contrario, Sensor.Leer(s) es un method expression; aquí tratamos al método como una función convencional donde el primer argumento debe ser explícitamente el tipo Sensor. Esta distinción es crucial cuando trabajas con tipos funcionales o inyección de dependencias, ya que Sensor.Leer tiene una firma de función que requiere un Sensor como primer parámetro.
Finalmente, fíjate en s.Valor.String(). Aunque Valor es un campo de la struct Sensor, su tipo es Celsius. Por eso podemos encadenar llamadas: primero accedemos al campo y luego invocamos el método definido específicamente para el tipo Celsius.
El error frecuente
Un error común es intentar extender tipos integrados o de otros paquetes. El compilador de Go es estricto para prevenir que un desarrollador altere el comportamiento de tipos base.
package main
// Error: cannot define new methods on non-local types
func (i int) EsPar() bool {
return i%2 == 0
}
// Error: cannot define new methods on non-local types
// (asumiendo que time es un paquete importado)
// func (t time.Time) MiMetodo() {}
func main() {}
Para solucionar esto, si necesitas añadir comportamiento a un int, debes crear un tipo derivado: type MyInt int. De esta forma, los métodos pertenecen a MyInt y no al tipo primitivo global.
N° 68