Skip to main content
โšก Calmops

Function Receivers and Methods

Function Receivers and Methods

In Go, methods are functions with a special receiver argument. Methods allow you to define behavior on types without inheritance. This guide covers receivers, methods, and best practices for designing methods in Go.

Understanding Receivers

Value Receivers

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

// Value receiver - receives a copy of the struct
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Value receiver - receives a copy
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    
    fmt.Println("Area:", rect.Area())           // 50
    fmt.Println("Perimeter:", rect.Perimeter()) // 30
}

Pointer Receivers

package main

import "fmt"

type Counter struct {
    Count int
}

// Pointer receiver - receives a pointer to the struct
func (c *Counter) Increment() {
    c.Count++
}

// Pointer receiver - can modify the original
func (c *Counter) Add(n int) {
    c.Count += n
}

// Value receiver - cannot modify the original
func (c Counter) GetCount() int {
    return c.Count
}

func main() {
    counter := Counter{Count: 0}
    
    counter.Increment()
    fmt.Println("Count:", counter.Count)  // 1
    
    counter.Add(5)
    fmt.Println("Count:", counter.Count)  // 6
    
    fmt.Println("Get:", counter.GetCount())  // 6
}

Choosing Between Value and Pointer Receivers

When to Use Value Receivers

package main

import "fmt"

type Point struct {
    X, Y float64
}

// Value receiver - good for small, immutable types
func (p Point) Distance() float64 {
    return (p.X*p.X + p.Y*p.Y) ^ 0.5
}

// Value receiver - good for simple queries
func (p Point) String() string {
    return fmt.Sprintf("(%f, %f)", p.X, p.Y)
}

func main() {
    p := Point{X: 3, Y: 4}
    fmt.Println(p.Distance())
    fmt.Println(p.String())
}

When to Use Pointer Receivers

package main

import "fmt"

type User struct {
    ID    int
    Name  string
    Email string
}

// Pointer receiver - needed to modify the struct
func (u *User) SetEmail(email string) {
    u.Email = email
}

// Pointer receiver - needed to modify the struct
func (u *User) UpdateName(name string) {
    u.Name = name
}

// Pointer receiver - for consistency with other methods
func (u *User) Save() error {
    // Save to database
    return nil
}

func main() {
    user := &User{ID: 1, Name: "Alice"}
    
    user.SetEmail("[email protected]")
    user.UpdateName("Alice Smith")
    user.Save()
    
    fmt.Printf("%+v\n", user)
}

Method Sets

Value vs Pointer Method Sets

package main

import "fmt"

type Animal struct {
    Name string
}

// Value receiver
func (a Animal) Speak() string {
    return fmt.Sprintf("%s makes a sound", a.Name)
}

// Pointer receiver
func (a *Animal) SetName(name string) {
    a.Name = name
}

func main() {
    dog := Animal{Name: "Dog"}
    
    // Can call value receiver on value
    fmt.Println(dog.Speak())
    
    // Can call pointer receiver on value (Go automatically takes address)
    dog.SetName("Puppy")
    fmt.Println(dog.Name)
    
    // Can call both on pointer
    dogPtr := &dog
    fmt.Println(dogPtr.Speak())
    dogPtr.SetName("Doggy")
}

Method Chaining

Implementing Fluent Interface

package main

import "fmt"

type QueryBuilder struct {
    query string
}

// Return pointer to enable chaining
func (qb *QueryBuilder) Select(fields string) *QueryBuilder {
    qb.query = "SELECT " + fields
    return qb
}

func (qb *QueryBuilder) From(table string) *QueryBuilder {
    qb.query += " FROM " + table
    return qb
}

func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
    qb.query += " WHERE " + condition
    return qb
}

func (qb *QueryBuilder) Build() string {
    return qb.query
}

func main() {
    query := (&QueryBuilder{}).
        Select("id, name, email").
        From("users").
        Where("age > 18").
        Build()
    
    fmt.Println(query)
    // Output: SELECT id, name, email FROM users WHERE age > 18
}

Receiver Types

Struct Receivers

package main

type Person struct {
    Name string
    Age  int
}

func (p Person) IsAdult() bool {
    return p.Age >= 18
}

func (p *Person) HaveBirthday() {
    p.Age++
}

Pointer Receivers

package main

type Database struct {
    connection string
}

func (db *Database) Connect() error {
    // Connect to database
    return nil
}

func (db *Database) Close() error {
    // Close connection
    return nil
}

Named Type Receivers

package main

import "fmt"

type MyInt int

func (i MyInt) IsEven() bool {
    return i%2 == 0
}

func (i MyInt) Double() MyInt {
    return i * 2
}

func main() {
    num := MyInt(5)
    fmt.Println(num.IsEven())  // false
    fmt.Println(num.Double())  // 10
}

Practical Examples

User Type with Methods

package main

import (
    "fmt"
    "strings"
)

type User struct {
    ID    int
    Name  string
    Email string
}

// Value receiver - query method
func (u User) IsValid() bool {
    return u.Name != "" && u.Email != ""
}

// Value receiver - query method
func (u User) String() string {
    return fmt.Sprintf("User{ID: %d, Name: %s, Email: %s}", u.ID, u.Name, u.Email)
}

// Pointer receiver - mutation method
func (u *User) SetEmail(email string) error {
    if !strings.Contains(email, "@") {
        return fmt.Errorf("invalid email: %s", email)
    }
    u.Email = email
    return nil
}

// Pointer receiver - mutation method
func (u *User) UpdateName(name string) {
    u.Name = name
}

func main() {
    user := &User{ID: 1, Name: "Alice"}
    
    fmt.Println(user.IsValid())
    fmt.Println(user.String())
    
    user.SetEmail("[email protected]")
    user.UpdateName("Alice Smith")
    
    fmt.Println(user.String())
}

Bank Account with Methods

package main

import "fmt"

type Account struct {
    Balance float64
}

// Value receiver - query
func (a Account) GetBalance() float64 {
    return a.Balance
}

// Pointer receiver - mutation
func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("deposit amount must be positive")
    }
    a.Balance += amount
    return nil
}

// Pointer receiver - mutation
func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("withdrawal amount must be positive")
    }
    if amount > a.Balance {
        return fmt.Errorf("insufficient funds")
    }
    a.Balance -= amount
    return nil
}

func main() {
    account := &Account{Balance: 1000}
    
    fmt.Println("Balance:", account.GetBalance())
    
    account.Deposit(500)
    fmt.Println("After deposit:", account.GetBalance())
    
    account.Withdraw(200)
    fmt.Println("After withdrawal:", account.GetBalance())
}

Best Practices

โœ… Good Practices

  1. Use value receivers for immutable types - Small, simple types
  2. Use pointer receivers for mutable types - When you need to modify
  3. Be consistent - Use pointer or value, not both
  4. Document receiver choice - Explain why you chose it
  5. Use pointer receivers for large structs - Avoid copying
  6. Implement String() with value receiver - Standard practice
  7. Use method chaining - For fluent interfaces
  8. Keep methods focused - Single responsibility

โŒ Anti-Patterns

// โŒ Bad: Mixing value and pointer receivers inconsistently
type User struct {
    Name string
}

func (u User) GetName() string {
    return u.Name
}

func (u *User) SetName(name string) {
    u.Name = name
}

// โœ… Good: Consistent receiver choice
type User struct {
    Name string
}

func (u *User) GetName() string {
    return u.Name
}

func (u *User) SetName(name string) {
    u.Name = name
}

// โŒ Bad: Unnecessary pointer receiver for immutable operation
func (u *User) GetAge() int {
    return u.Age
}

// โœ… Good: Value receiver for query
func (u User) GetAge() int {
    return u.Age
}

Summary

Methods and receivers are fundamental:

  • Use value receivers for immutable operations
  • Use pointer receivers for mutations
  • Be consistent with receiver choice
  • Implement String() with value receiver
  • Use method chaining for fluent interfaces
  • Keep methods focused and simple

Master methods for effective Go design.

Comments