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
- Use value receivers for immutable types - Small, simple types
- Use pointer receivers for mutable types - When you need to modify
- Be consistent - Use pointer or value, not both
- Document receiver choice - Explain why you chose it
- Use pointer receivers for large structs - Avoid copying
- Implement String() with value receiver - Standard practice
- Use method chaining - For fluent interfaces
- 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