Skip to main content

Command Palette

Search for a command to run...

Métodos y Receptores en Go: Value vs Pointer Receivers

Updated
8 min read

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

AspectoJavaGo
ReceptoresSiempre referencia implícitaExplícito: value o pointer
ControlNo tienes controlControl total
CopiasAutomáticas en primitivosExplícitas con value receiver
PerformanceOptimizado por JVMControl explícito del desarrollador
ConsistenciaNo aplicaImportante 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

EscenarioReceiver RecomendadoRazón
Método modifica el structPointer *TNecesitas modificar el original
Struct grande (>100 bytes)Pointer *TEvita copias costosas
Método solo lee, struct pequeñoValue TMás simple, inmutable
Método solo lee, struct grandePointer *TConsistencia y performance
Implementar String()Value TConvención común
Todos los métodos modificanPointer *TConsistencia

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

  1. Sé consistente: Si un método usa pointer receiver, todos deberían usarlo

  2. Piensa en el tamaño: Structs grandes → pointer receiver

  3. Piensa en mutabilidad: Si modificas → pointer receiver

  4. Excepciones: String() y métodos similares pueden usar value receiver

  5. Cuando 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

More from this blog

JoeDayz

53 posts

Community Guy | Java Champion | AWS Architect | Software Architect