Un método en Go es una función asociada a un tipo mediante un receptor. Ese receptor indica sobre qué valor trabaja el método.
En el artículo anterior vimos cómo crear structs para agrupar datos. Ahora vamos a asociarles comportamiento mediante métodos.
En lenguajes como Java o C#, los métodos se definen dentro de la clase (class Persona { void Saludar() {...} }).
En Go, como no hay clases, los métodos se declaran fuera del struct, con un receptor que los asocia al tipo.
Vamos a ver cómo declarar un método y cuándo conviene usar un receptor por valor o por puntero.
Anatomía de un método
Un método tiene una sección de receptor situada antes del nombre. El receptor se comporta como un parámetro dentro del cuerpo del método.
Sintaxis:
func (r Receptor) NombreMetodo() Retorno { ... }
package main
import "fmt"
type Triangulo struct {
Base, Altura float64
}
// Esto es una FUNCIÓN normal
func AreaFuncion(t Triangulo) float64 {
return (t.Base * t.Altura) / 2
}
// Esto es un MÉTODO enganchado a Triangulo
// (t Triangulo) es el receptor
func (t Triangulo) Area() float64 {
return (t.Base * t.Altura) / 2
}
func main() {
miTriangulo := Triangulo{10, 5}
// Llamada al método (estilo Orientado a Objetos)
fmt.Println(miTriangulo.Area()) // 25
}El nombre del receptor (t en el ejemplo) puede ser cualquiera, pero por convención en Go se suele usar la primera letra del tipo en minúscula (ej: t para Triangulo, c para Client, etc.). Evita usar nombres genéricos como this o self.
Receptor por valor o por puntero
Al definir un método, tienes dos opciones para el receptor:
- Receptor por valor:
(t Triangulo) - Receptor por puntero:
(t *Triangulo)
Esta decisión determina qué valor se copia y qué conjunto de métodos implementan T y *T.
Receptor por valor
Cuando usas un receptor por valor, el método recibe una COPIA de la estructura.
Consecuencias:
- Campos directos: si asignas un campo del receptor, modificas la copia y el valor original no cambia.
- Datos referenciados: si un campo contiene un mapa, slice o puntero, sus datos subyacentes pueden seguir compartidos; un receptor por valor no garantiza inmutabilidad profunda.
- Coste: Si el struct es muy grande (ej: tiene un array de 1MB dentro), se copiará entero cada vez que llames al método.
type Contador struct {
Valor int
}
// Receptor por Valor (sin *)
func (c Contador) IncrementarMal() {
c.Valor++ // Incrementamos LA COPIA
fmt.Println("Dentro del método:", c.Valor)
}
func main() {
c := Contador{0}
c.IncrementarMal() // Imprime: Dentro: 1
fmt.Println("Fuera (Original):", c.Valor) // Imprime: 0 (¡No ha cambiado!)
}Receptor por puntero
Cuando pones el asterisco *, el método recibe un puntero a la instancia original.
Consecuencias:
- Mutabilidad: Cualquier cambio que hagas afectará a la variable original.
- Copia: se copia la dirección en lugar de toda la estructura, aunque eso no garantiza por sí solo un mejor rendimiento.
// Receptor por Puntero (con *)
func (c *Contador) IncrementarBien() {
c.Valor++ // Go desreferencia automáticamente y modifica el original
}
func main() {
c := Contador{0}
c.IncrementarBien()
fmt.Println("Original:", c.Valor) // Imprime: 1 (¡Ahora sí!)
}La comodidad de la llamada automática
Podrías pensar: “Si el método espera un puntero, ¿tengo que llamarlo usando (&c).Incrementar()?”
Afortunadamente, no. Go es muy listo.
Al llamar a un método de puntero sobre una variable direccionable, Go toma su dirección automáticamente. También permite llamar a los métodos de valor a través de un puntero.
c := Contador{0} // c es un Valor
c.IncrementarBien() // Go lo traduce a (&c).IncrementarBien()
p := &Contador{0} // p es un Puntero
p.IncrementarMal() // Go lo traduce a (*p).IncrementarMal()Esta comodidad tiene un límite: un valor temporal no direccionable, como Contador{}, no puede llamar directamente a un método cuyo receptor sea *Contador.
Cómo elegir el receptor
Este árbol de decisión ayuda bastante:
¿El método necesita modificar el estado del objeto?
- SÍ ➔ Usa Puntero
(p *Persona). - NO ➔ Sigue leyendo.
¿El struct es grande o contiene un campo que no debe copiarse, como sync.Mutex?
- SÍ ➔ Usa Puntero
(p *Persona)para evitar copiarlo todo. - NO ➔ Puedes usar Valor.
¿Consistencia?
- Si ALGÚN método del tipo necesita ser puntero, se recomienda poner TODOS como punteros. Esto evita mezclas raras y hace que la interfaz del tipo sea predecible.
:::
Para tipos pequeños con semántica de valor, como coordenadas o duraciones, un receptor por valor suele ser natural. Para mutar el receptor, evitar copias grandes o proteger tipos que no deben copiarse, usa puntero. No elijas puntero solo por intuición de rendimiento: mide si es relevante.
Receptores en tipos que no son structs
Puedes definir métodos en un tipo definido en el mismo paquete, siempre que el tipo base no sea un puntero ni una interfaz.
Esto permite cosas muy potentes, como añadir lógica a un simple float o a una función.
// Definimos un tipo propio basado en float64
type Dinero float64
// Le añadimos un método para formatearse
func (d Dinero) String() string {
return fmt.Sprintf("%.2f €", d)
}
func main() {
var precio Dinero = 25.5
fmt.Println(precio.String()) // "25.50 €"
}