Métodos y receivers (valor vs puntero)
Un método en Go es una función con un receiver: el tipo al que pertenece. La decisión entre receiver por valor o por puntero cambia si las mutaciones persisten y si se copia memoria. Aquí va la guía práctica.
Definir un método con receiver
Un método se declara como una función normal, pero con un parámetro extra entre func y el nombre: el receiver. Ese receiver determina el tipo al que pertenece el método.
package main
import (
"fmt"
"math"
)
type Point struct {
X, Y float64
}
// Método con receiver por valor — recibe una COPIA de Point
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// Otro método sobre Point
func (p Point) Quadrant() int {
switch {
case p.X >= 0 && p.Y >= 0:
return 1
case p.X < 0 && p.Y >= 0:
return 2
case p.X < 0 && p.Y < 0:
return 3
default:
return 4
}
}
func main() {
p := Point{3, 4}
fmt.Println(p.Distance()) // 5
fmt.Println(p.Quadrant()) // 1
q := Point{-1, -2}
fmt.Println(q.Quadrant()) // 3
}5
3En Go, cualquier tipo definido en tu paquete puede tener métodos — no solo structs. También puedes asociar métodos a alias de tipos básicos, como type Celsius float64.
Receiver por valor vs por puntero
- Por valor (
func (p Point) ...): el método recibe una copia. Las mutaciones no se ven fuera. - Por puntero (
func (p *Point) ...): el método recibe la dirección. Puede mutar el original.
Usa puntero cuando quieras mutar el receiver o cuando el struct sea grande (evita copias).
package main
import "fmt"
type Counter struct {
Value int
}
// Receiver por VALOR: recibe copia — las mutaciones NO persisten
func (c Counter) IncByValue() {
c.Value++
}
// Receiver por PUNTERO: recibe la dirección — las mutaciones SÍ persisten
func (c *Counter) IncByPointer() {
c.Value++
}
// Move muta el Point original — necesita puntero
type Point struct{ X, Y int }
func (p *Point) Move(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
c := Counter{Value: 0}
c.IncByValue()
c.IncByValue()
fmt.Println(c.Value) // 0 — la copia se descartó
c.IncByPointer()
c.IncByPointer()
fmt.Println(c.Value) // 2 — mutación real
// Auto-address: aunque p no es puntero, Go inserta & automáticamente
p := Point{1, 1}
p.Move(10, 20) // equivalente a (&p).Move(10, 20)
fmt.Println(p) // {11 21}
}0
2
{11 21}Consistencia: no mezcles receivers
Para un mismo tipo, no mezcles receivers por valor y por puntero. Si algún método necesita puntero, ponlos todos por puntero. La razón es predictibilidad y compatibilidad con interfaces.
package main
import "fmt"
type Account struct {
Owner string
Balance float64
}
// Todos los métodos por PUNTERO — coherencia
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func (a *Account) Withdraw(amount float64) bool {
if amount > a.Balance {
return false
}
a.Balance -= amount
return true
}
func (a *Account) Summary() string {
return fmt.Sprintf("%s: %.2f€", a.Owner, a.Balance)
}
func main() {
acc := &Account{Owner: "Ana", Balance: 100}
acc.Deposit(50)
fmt.Println(acc.Summary()) // Ana: 150.00€
ok := acc.Withdraw(200)
fmt.Println(ok) // false — saldo insuficiente
fmt.Println(acc.Summary()) // Ana: 150.00€
acc.Withdraw(75)
fmt.Println(acc.Summary()) // Ana: 75.00€
}Ana: 150.00€
false
Ana: 150.00€
Ana: 75.00€- ¿El método muta el receiver? → puntero.
- ¿El struct es grande (varios campos, slices, maps)? → puntero.
- ¿El tipo es pequeño e inmutable (
Point,time.Time)? → valor está bien. - En la duda, puntero. Y siempre consistente para un mismo tipo.
Métodos en tipos derivados (no solo structs)
Puedes definir métodos en cualquier tipo declarado en tu paquete. Es muy común envolver tipos primitivos para añadir semántica.
package main
import "fmt"
// Tipo derivado de float64 con semántica de "grados Celsius"
type Celsius float64
func (c Celsius) ToFahrenheit() float64 {
return float64(c)*9/5 + 32
}
func (c Celsius) IsFreezing() bool {
return c <= 0
}
// Tipo derivado sobre slice
type IntList []int
func (l IntList) Sum() int {
total := 0
for _, n := range l {
total += n
}
return total
}
func main() {
t := Celsius(25)
fmt.Println(t.ToFahrenheit()) // 77
fmt.Println(t.IsFreezing()) // false
nums := IntList{1, 2, 3, 4}
fmt.Println(nums.Sum()) // 10
}77
false
10Go no tiene extends ni super. La forma de reutilizar comportamiento es embebido (los métodos del tipo embebido se promueven) e interfaces (polimorfismo sin jerarquías).