Métodos y Receptores en Go: Value vs Pointer Receivers
En los posts anteriores exploramos los structs y las interfaces implícitas en Go. Hoy profundizaremos en cómo agregar comportamiento a nuestros tipos mediante métodos y entenderemos una de las decisiones más importantes en Go: cuándo usar value receivers vs pointer receivers.
Si vienes de Java, este concepto puede ser nuevo para ti, ya que en Java todos los métodos trabajan con referencias implícitas. En Go, tienes control explícito sobre esto, y elegir correctamente es crucial para el rendimiento y la corrección de tu código.
¿Qué son los Métodos en Go?
En Go, puedes definir métodos en cualquier tipo, no solo en structs. Un método es simplemente una función con un receptor especial:
type Counter struct {
value int
}
// Método con value receiver
func (c Counter) GetValue() int {
return c.value
}
// Método con pointer receiver
func (c *Counter) Increment() {
c.value++
}
La diferencia clave está en el receptor: (c Counter) vs (c *Counter).
Value Receiver: Trabajando con Copias
Un value receiver recibe una copia del valor. Cualquier modificación que hagas dentro del método no afecta al valor original:
type Counter struct {
value int
}
// Value receiver - recibe una COPIA
func (c Counter) Increment() {
c.value++ // Solo modifica la copia
fmt.Printf("Inside method: %d\n", c.value)
}
func main() {
counter := Counter{value: 0}
fmt.Printf("Before: %d\n", counter.value)
counter.Increment()
fmt.Printf("After: %d\n", counter.value) // ¡Sigue siendo 0!
}
Salida:
Before: 0
Inside method: 1
After: 0 ← No cambió el original
¿Cuándo usar Value Receiver?
✅ Usa value receiver cuando:
El struct es pequeño (pocos campos, tipos primitivos)
El método solo lee datos, no los modifica
Quieres garantizar inmutabilidad
El método es una función pura (sin efectos secundarios)
type Point struct {
X, Y float64
}
// Value receiver es perfecto aquí
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
Pointer Receiver: Trabajando con Referencias
Un pointer receiver recibe una referencia al valor original. Las modificaciones afectan al valor original:
type CounterPtr struct {
value int
}
// Pointer receiver - recibe una REFERENCIA
func (c *CounterPtr) Increment() {
c.value++ // Modifica el original
fmt.Printf("Inside method: %d\n", c.value)
}
func main() {
counter := CounterPtr{value: 0}
fmt.Printf("Before: %d\n", counter.value)
counter.Increment()
fmt.Printf("After: %d\n", counter.value) // ¡Ahora sí cambió!
}
Salida:
Before: 0
Inside method: 1
After: 1 ← Sí cambió el original
¿Cuándo usar Pointer Receiver?
✅ Usa pointer receiver cuando:
El método modifica el struct
El struct es grande (evita copias costosas)
Quieres consistencia (si un método modifica, todos deberían usar pointer)
Necesitas que los cambios persistan
type Account struct {
ID string
Balance float64
Owner string
}
// Pointer receiver necesario porque modifica
func (a *Account) Deposit(amount float64) error {
if amount <= 0 {
return fmt.Errorf("amount must be positive")
}
a.Balance += amount // Modifica el original
return nil
}
// También pointer receiver por consistencia
func (a *Account) GetBalance() float64 {
return a.Balance
}
Regla de Oro: Consistencia
Una regla importante en Go es ser consistente. Si un método de un struct usa pointer receiver porque modifica el struct, todos los métodos de ese struct deberían usar pointer receiver, incluso los que solo leen:
type LargeStruct struct {
data [1000]int // Struct grande
name string
}
// Modifica → pointer receiver
func (ls *LargeStruct) UpdateName(name string) {
ls.name = name
}
// Solo lee, pero usa pointer receiver por consistencia
func (ls *LargeStruct) GetName() string {
return ls.name
}
Esto hace que el código sea más predecible y evita confusiones.
Métodos en Tipos No-Struct
Una característica poderosa de Go es que puedes definir métodos en cualquier tipo, no solo structs:
// Tipo personalizado basado en int
type MyInt int
// Método en el tipo personalizado
func (m MyInt) Double() MyInt {
return m * 2
}
// Implementar interfaces (como Stringer)
func (m MyInt) String() string {
return fmt.Sprintf("MyInt(%d)", int(m))
}
func main() {
num := MyInt(5)
fmt.Println(num) // MyInt(5)
fmt.Println(num.Double()) // MyInt(10)
}
Métodos en Slices Personalizados
type IntSlice []int
func (is IntSlice) Sum() int {
sum := 0
for _, v := range is {
sum += v
}
return sum
}
func (is IntSlice) Append(value int) IntSlice {
return append(is, value)
}
func main() {
numbers := IntSlice{1, 2, 3, 4, 5}
fmt.Println(numbers.Sum()) // 15
numbers = numbers.Append(6)
fmt.Println(numbers) // [1 2 3 4 5 6]
}
Conversión Automática: La Magia de Go
Go hace algo muy inteligente: convierte automáticamente entre valores y punteros cuando llamas métodos:
type Account struct {
Balance float64
}
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func main() {
// Tienes un value
acc := Account{Balance: 1000}
// Puedes llamar método con pointer receiver directamente
acc.Deposit(100) // Go automáticamente hace: (&acc).Deposit(100)
// Tienes un pointer
accPtr := &Account{Balance: 500}
// Puedes llamar método con value receiver directamente
fmt.Println(accPtr.String()) // Go automáticamente hace: (*accPtr).String()
}
Esto significa que no necesitas preocuparte por si tienes un valor o un puntero al llamar métodos. Go lo maneja automáticamente.
Ejemplo Práctico: Sistema Bancario
Vamos a construir un sistema de cuentas bancarias que demuestra el uso correcto de receptores:
type Account struct {
ID string
Balance float64
Owner string
}
// Métodos que modifican → pointer receiver
func (a *Account) Deposit(amount float64) error {
if amount <= 0 {
return fmt.Errorf("deposit amount must be positive")
}
a.Balance += amount
return nil
}
func (a *Account) Withdraw(amount float64) error {
if amount <= 0 {
return fmt.Errorf("withdrawal amount must be positive")
}
if a.Balance < amount {
return fmt.Errorf("insufficient funds")
}
a.Balance -= amount
return nil
}
// Método que solo lee → pero usa pointer por consistencia
func (a *Account) GetBalance() float64 {
return a.Balance
}
// Método que solo lee → value receiver está bien
func (a Account) String() string {
return fmt.Sprintf("Account{ID: %s, Owner: %s, Balance: $%.2f}",
a.ID, a.Owner, a.Balance)
}
func main() {
account := &Account{
ID: "ACC001",
Balance: 1000.0,
Owner: "John Doe",
}
account.Deposit(500.0)
account.Withdraw(200.0)
fmt.Println(account) // Account{ID: ACC001, Owner: John Doe, Balance: $1300.00}
}
Comparación: Java vs Go
| Aspecto | Java | Go |
| Receptores | Siempre referencia implícita | Explícito: value o pointer |
| Control | No tienes control | Control total |
| Copias | Automáticas en primitivos | Explícitas con value receiver |
| Performance | Optimizado por JVM | Control explícito del desarrollador |
| Consistencia | No aplica | Importante mantener consistencia |
En Java:
public class Counter {
private int value;
public void increment() {
this.value++; // Siempre modifica el original
}
}
En Go:
type Counter struct {
value int
}
// Puedes elegir:
func (c Counter) Increment() { ... } // Copia
func (c *Counter) Increment() { ... } // Referencia
Decisiones de Diseño: Tabla de Referencia
| Escenario | Receiver Recomendado | Razón |
| Método modifica el struct | Pointer *T | Necesitas modificar el original |
| Struct grande (>100 bytes) | Pointer *T | Evita copias costosas |
| Método solo lee, struct pequeño | Value T | Más simple, inmutable |
| Método solo lee, struct grande | Pointer *T | Consistencia y performance |
Implementar String() | Value T | Convención común |
| Todos los métodos modifican | Pointer *T | Consistencia |
Errores Comunes
❌ Error 1: Usar value receiver cuando necesitas modificar
// ❌ MAL
func (c Counter) Increment() {
c.value++ // No funciona, solo modifica la copia
}
// ✅ BIEN
func (c *Counter) Increment() {
c.value++ // Modifica el original
}
❌ Error 2: Mezclar value y pointer receivers sin razón
// ❌ MAL - Inconsistente
func (a *Account) Deposit(amount float64) { ... } // Pointer
func (a Account) GetBalance() float64 { ... } // Value
// ✅ BIEN - Consistente
func (a *Account) Deposit(amount float64) { ... } // Pointer
func (a *Account) GetBalance() float64 { ... } // Pointer
Mejores Prácticas
Sé consistente: Si un método usa pointer receiver, todos deberían usarlo
Piensa en el tamaño: Structs grandes → pointer receiver
Piensa en mutabilidad: Si modificas → pointer receiver
Excepciones:
String()y métodos similares pueden usar value receiverCuando dudes: Usa pointer receiver (es más seguro)
Conclusiones
Los métodos y receptores en Go te dan control explícito sobre cómo se pasan los valores:
✅ Value receivers: Para structs pequeños, métodos inmutables, y cuando quieres garantizar que no se modifique el original
✅ Pointer receivers: Para métodos que modifican, structs grandes, y cuando quieres consistencia
✅ Conversión automática: Go maneja automáticamente las conversiones entre valores y punteros, haciendo el código más limpio
✅ Consistencia: Mantén consistencia en tu código usando el mismo tipo de receiver para todos los métodos de un struct
Esta decisión puede parecer pequeña, pero tiene implicaciones importantes en el rendimiento y la corrección de tu código. Una vez que entiendas cuándo usar cada uno, escribirás código Go más eficiente y mantenible.
Próximos Pasos
En el siguiente post exploraremos las colecciones en Go: slices, maps y arrays. Aprenderemos sobre len, cap, make, y cómo trabajar eficientemente con estructuras de datos en Go.
¿Tienes preguntas sobre value vs pointer receivers? Déjame saber en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.
Enjoy!
José Díaz
+51 939965148




