Skip to main content

Go Methods and Receivers

Methods in Go are functions that are attached to specific types, allowing you to add behavior to structs and other custom types. Go's method system is unique in its approach to object-oriented programming, using receivers to bind functions to types. Understanding methods and receivers is fundamental to writing idiomatic Go code and implementing object-oriented patterns. This comprehensive guide will teach you everything you need to know about Go's method system.

Understanding Methods in Go

What Are Methods?

Methods in Go are functions that have a special receiver parameter that binds the function to a specific type. They provide:

  • Type-specific behavior - Functions that operate on specific types
  • Encapsulation - Data and behavior grouped together
  • Code organization - Related functionality organized by type
  • Polymorphism - Different types can implement the same method interface
  • Clean APIs - Intuitive interfaces for type operations

Go's Method System Characteristics

Go's method system has several distinctive features:

Receiver Syntax

Methods use a special receiver parameter syntax that binds them to types.

Value vs Pointer Receivers

Methods can have value receivers (copy) or pointer receivers (reference).

Method Sets

Go defines method sets that determine which methods are available on types.

Implicit Interface Satisfaction

Methods enable types to satisfy interfaces implicitly.

Method Definition and Syntax

Basic Method Definition

The func Keyword with Receivers

Methods are defined using the func keyword with a receiver parameter before the function name.

Receiver Parameter Syntax

The receiver parameter specifies which type the method belongs to.

package main

import "fmt"

func main() {
// Basic method definition and syntax examples
fmt.Println("Basic method definition and syntax examples:")

// Define a struct
type Rectangle struct {
Width float64
Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

// Method with value receiver
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

// Method with value receiver
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle{Width: %.2f, Height: %.2f}", r.Width, r.Height)
}

// Create rectangle instance
rect := Rectangle{Width: 5.0, Height: 3.0}

// Call methods
fmt.Printf("Rectangle: %s\n", rect.String())
fmt.Printf("Area: %.2f\n", rect.Area())
fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
// Output:
// Rectangle: Rectangle{Width: 5.00, Height: 3.00}
// Area: 15.00
// Perimeter: 16.00

// Define another struct
type Circle struct {
Radius float64
}

// Method with value receiver
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}

// Method with value receiver
func (c Circle) Circumference() float64 {
return 2 * 3.14159 * c.Radius
}

// Method with value receiver
func (c Circle) String() string {
return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}

// Create circle instance
circle := Circle{Radius: 2.5}

// Call methods
fmt.Printf("Circle: %s\n", circle.String())
fmt.Printf("Area: %.2f\n", circle.Area())
fmt.Printf("Circumference: %.2f\n", circle.Circumference())
// Output:
// Circle: Circle{Radius: 2.50}
// Area: 19.63
// Circumference: 15.71
}

Method with Different Receiver Types

Value Receivers

Value receivers receive a copy of the struct value.

Pointer Receivers

Pointer receivers receive a reference to the struct value.

package main

import "fmt"

func main() {
// Method with different receiver types examples
fmt.Println("Method with different receiver types examples:")

// Define a struct
type Counter struct {
value int
}

// Method with value receiver (read-only)
func (c Counter) GetValue() int {
return c.value
}

// Method with value receiver (read-only)
func (c Counter) String() string {
return fmt.Sprintf("Counter{value: %d}", c.value)
}

// Method with pointer receiver (can modify)
func (c *Counter) Increment() {
c.value++
}

// Method with pointer receiver (can modify)
func (c *Counter) Decrement() {
c.value--
}

// Method with pointer receiver (can modify)
func (c *Counter) SetValue(value int) {
c.value = value
}

// Method with pointer receiver (can modify)
func (c *Counter) Reset() {
c.value = 0
}

// Create counter instance
counter := Counter{value: 0}

// Use value receiver methods
fmt.Printf("Initial counter: %s\n", counter.String())
fmt.Printf("Initial value: %d\n", counter.GetValue())
// Output:
// Initial counter: Counter{value: 0}
// Initial value: 0

// Use pointer receiver methods
counter.Increment()
counter.Increment()
counter.Increment()
fmt.Printf("After incrementing: %s\n", counter.String())
// Output: After incrementing: Counter{value: 3}

counter.Decrement()
fmt.Printf("After decrementing: %s\n", counter.String())
// Output: After decrementing: Counter{value: 2}

counter.SetValue(10)
fmt.Printf("After setting value: %s\n", counter.String())
// Output: After setting value: Counter{value: 10}

counter.Reset()
fmt.Printf("After reset: %s\n", counter.String())
// Output: After reset: Counter{value: 0}

// Demonstrate value vs pointer receiver behavior
fmt.Println("\nValue vs pointer receiver behavior:")

// Create a new counter
counter2 := Counter{value: 5}

// Value receiver method doesn't modify original
func (c Counter) TryModify() {
c.value = 100 // This doesn't affect the original
}

counter2.TryModify()
fmt.Printf("After TryModify (value receiver): %s\n", counter2.String())
// Output: After TryModify (value receiver): Counter{value: 5}

// Pointer receiver method modifies original
func (c *Counter) ActuallyModify() {
c.value = 100 // This affects the original
}

counter2.ActuallyModify()
fmt.Printf("After ActuallyModify (pointer receiver): %s\n", counter2.String())
// Output: After ActuallyModify (pointer receiver): Counter{value: 100}
}

Value vs Pointer Receivers

Understanding Receiver Types

Value Receivers

Value receivers receive a copy of the struct value, making them safe for concurrent access but unable to modify the original.

Pointer Receivers

Pointer receivers receive a reference to the struct value, allowing them to modify the original but requiring careful concurrent access.

package main

import "fmt"

func main() {
// Value vs pointer receivers examples
fmt.Println("Value vs pointer receivers examples:")

// Define a struct
type BankAccount struct {
accountNumber string
balance float64
owner string
}

// Value receiver methods (read-only operations)
func (ba BankAccount) GetAccountNumber() string {
return ba.accountNumber
}

func (ba BankAccount) GetBalance() float64 {
return ba.balance
}

func (ba BankAccount) GetOwner() string {
return ba.owner
}

func (ba BankAccount) String() string {
return fmt.Sprintf("Account %s (Owner: %s, Balance: $%.2f)",
ba.accountNumber, ba.owner, ba.balance)
}

// Pointer receiver methods (modifying operations)
func (ba *BankAccount) Deposit(amount float64) error {
if amount <= 0 {
return fmt.Errorf("deposit amount must be positive")
}
ba.balance += amount
return nil
}

func (ba *BankAccount) Withdraw(amount float64) error {
if amount <= 0 {
return fmt.Errorf("withdrawal amount must be positive")
}
if amount > ba.balance {
return fmt.Errorf("insufficient funds")
}
ba.balance -= amount
return nil
}

func (ba *BankAccount) Transfer(amount float64, target *BankAccount) error {
if err := ba.Withdraw(amount); err != nil {
return err
}
return target.Deposit(amount)
}

func (ba *BankAccount) SetOwner(newOwner string) {
ba.owner = newOwner
}

// Create bank account
account := &BankAccount{
accountNumber: "12345",
balance: 1000.0,
owner: "Alice",
}

// Use value receiver methods
fmt.Printf("Account: %s\n", account.String())
fmt.Printf("Owner: %s\n", account.GetOwner())
fmt.Printf("Balance: $%.2f\n", account.GetBalance())
// Output:
// Account: Account 12345 (Owner: Alice, Balance: $1000.00)
// Owner: Alice
// Balance: $1000.00

// Use pointer receiver methods
if err := account.Deposit(500.0); err != nil {
fmt.Printf("Deposit error: %v\n", err)
} else {
fmt.Printf("After deposit: $%.2f\n", account.GetBalance())
}
// Output: After deposit: $1500.00

if err := account.Withdraw(200.0); err != nil {
fmt.Printf("Withdrawal error: %v\n", err)
} else {
fmt.Printf("After withdrawal: $%.2f\n", account.GetBalance())
}
// Output: After withdrawal: $1300.00

// Create another account for transfer
targetAccount := &BankAccount{
accountNumber: "67890",
balance: 500.0,
owner: "Bob",
}

fmt.Printf("Target account before transfer: $%.2f\n", targetAccount.GetBalance())
// Output: Target account before transfer: $500.00

if err := account.Transfer(300.0, targetAccount); err != nil {
fmt.Printf("Transfer error: %v\n", err)
} else {
fmt.Printf("After transfer - Source: $%.2f, Target: $%.2f\n",
account.GetBalance(), targetAccount.GetBalance())
}
// Output: After transfer - Source: $1000.00, Target: $800.00

// Change owner
account.SetOwner("Alice Smith")
fmt.Printf("New owner: %s\n", account.GetOwner())
// Output: New owner: Alice Smith
}

When to Use Value vs Pointer Receivers

Understanding when to use value vs pointer receivers is crucial for writing efficient and correct Go code.

package main

import "fmt"

func main() {
// When to use value vs pointer receivers examples
fmt.Println("When to use value vs pointer receivers examples:")

// Define a struct
type Point struct {
X, Y float64
}

// Value receiver methods (when you don't need to modify)
func (p Point) DistanceFromOrigin() float64 {
return p.X*p.X + p.Y*p.Y
}

func (p Point) DistanceTo(other Point) float64 {
dx := p.X - other.X
dy := p.Y - other.Y
return dx*dx + dy*dy
}

func (p Point) String() string {
return fmt.Sprintf("Point(%.2f, %.2f)", p.X, p.Y)
}

// Pointer receiver methods (when you need to modify)
func (p *Point) Move(dx, dy float64) {
p.X += dx
p.Y += dy
}

func (p *Point) SetPosition(x, y float64) {
p.X = x
p.Y = y
}

func (p *Point) Scale(factor float64) {
p.X *= factor
p.Y *= factor
}

// Create points
point1 := Point{X: 3.0, Y: 4.0}
point2 := Point{X: 0.0, Y: 0.0}

// Use value receiver methods
fmt.Printf("Point 1: %s\n", point1.String())
fmt.Printf("Distance from origin: %.2f\n", point1.DistanceFromOrigin())
fmt.Printf("Distance to point 2: %.2f\n", point1.DistanceTo(point2))
// Output:
// Point 1: Point(3.00, 4.00)
// Distance from origin: 25.00
// Distance to point 2: 25.00

// Use pointer receiver methods
point1.Move(1.0, 1.0)
fmt.Printf("After moving (1, 1): %s\n", point1.String())
// Output: After moving (1, 1): Point(4.00, 5.00)

point1.Scale(2.0)
fmt.Printf("After scaling by 2: %s\n", point1.String())
// Output: After scaling by 2: Point(8.00, 10.00)

point1.SetPosition(0.0, 0.0)
fmt.Printf("After setting position: %s\n", point1.String())
// Output: After setting position: Point(0.00, 0.00)

// Guidelines for choosing receiver types:
fmt.Println("\nGuidelines for choosing receiver types:")
fmt.Println("1. Use value receivers when:")
fmt.Println(" - The method doesn't need to modify the receiver")
fmt.Println(" - The struct is small (copying is cheap)")
fmt.Println(" - You want to ensure immutability")
fmt.Println(" - The method is read-only")

fmt.Println("\n2. Use pointer receivers when:")
fmt.Println(" - The method needs to modify the receiver")
fmt.Println(" - The struct is large (copying is expensive)")
fmt.Println(" - You want to maintain consistency")
fmt.Println(" - The method is part of a mutating interface")

fmt.Println("\n3. Be consistent:")
fmt.Println(" - Use the same receiver type for all methods on a type")
fmt.Println(" - Mixing receiver types can be confusing")
fmt.Println(" - Pointer receivers are more common in practice")
}

Method Sets and Method Promotion

Understanding Method Sets

Method Sets in Go

Go defines method sets that determine which methods are available on types.

Method Set Rules

The method set rules determine which methods can be called on a type.

package main

import "fmt"

func main() {
// Method sets and method promotion examples
fmt.Println("Method sets and method promotion examples:")

// Define a struct
type Person struct {
Name string
Age int
}

// Method with value receiver
func (p Person) GetName() string {
return p.Name
}

// Method with value receiver
func (p Person) GetAge() int {
return p.Age
}

// Method with value receiver
func (p Person) String() string {
return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}

// Method with pointer receiver
func (p *Person) SetName(name string) {
p.Name = name
}

// Method with pointer receiver
func (p *Person) SetAge(age int) {
p.Age = age
}

// Method with pointer receiver
func (p *Person) HaveBirthday() {
p.Age++
}

// Create person instance
person := Person{Name: "Alice", Age: 30}

// Call methods with value receiver
fmt.Printf("Person: %s\n", person.String())
fmt.Printf("Name: %s\n", person.GetName())
fmt.Printf("Age: %d\n", person.GetAge())
// Output:
// Person: Person{Name: Alice, Age: 30}
// Name: Alice
// Age: 30

// Call methods with pointer receiver
person.SetName("Alice Smith")
person.SetAge(31)
person.HaveBirthday()

fmt.Printf("After modifications: %s\n", person.String())
// Output: After modifications: Person{Name: Alice Smith, Age: 32}

// Method set rules demonstration
fmt.Println("\nMethod set rules demonstration:")

// Create a pointer to Person
personPtr := &Person{Name: "Bob", Age: 25}

// Both value and pointer receivers can be called on pointer
fmt.Printf("Pointer - Name: %s\n", personPtr.GetName())
fmt.Printf("Pointer - Age: %d\n", personPtr.GetAge())
personPtr.SetName("Bob Johnson")
personPtr.HaveBirthday()
fmt.Printf("Pointer after modifications: %s\n", personPtr.String())
// Output:
// Pointer - Name: Bob
// Pointer - Age: 25
// Pointer after modifications: Person{Name: Bob Johnson, Age: 26}

// Create a value of Person
personValue := Person{Name: "Charlie", Age: 35}

// Only value receivers can be called on value
fmt.Printf("Value - Name: %s\n", personValue.GetName())
fmt.Printf("Value - Age: %d\n", personValue.GetAge())
fmt.Printf("Value: %s\n", personValue.String())
// Output:
// Value - Name: Charlie
// Value - Age: 35
// Value: Person{Name: Charlie, Age: 35}

// Note: You cannot call pointer receiver methods on values
// personValue.SetName("New Name") // This would cause a compilation error

// Method set rules:
fmt.Println("\nMethod set rules:")
fmt.Println("1. Value type T has methods with value receivers")
fmt.Println("2. Pointer type *T has methods with both value and pointer receivers")
fmt.Println("3. Value receivers can be called on both values and pointers")
fmt.Println("4. Pointer receivers can only be called on pointers")
}

Method Promotion Through Embedding

Embedded Methods

Methods from embedded structs are promoted to the embedding struct.

Method Resolution Order

Go follows specific rules for resolving method calls when there are conflicts.

package main

import "fmt"

func main() {
// Method promotion through embedding examples
fmt.Println("Method promotion through embedding examples:")

// Define base struct
type Animal struct {
Name string
Age int
}

// Method with value receiver
func (a Animal) GetName() string {
return a.Name
}

// Method with value receiver
func (a Animal) GetAge() int {
return a.Age
}

// Method with value receiver
func (a Animal) String() string {
return fmt.Sprintf("Animal{Name: %s, Age: %d}", a.Name, a.Age)
}

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

// Method with pointer receiver
func (a *Animal) SetAge(age int) {
a.Age = age
}

// Define embedding struct
type Dog struct {
Animal // Embedded struct
Breed string
}

// Method with value receiver (overrides embedded method)
func (d Dog) String() string {
return fmt.Sprintf("Dog{Name: %s, Age: %d, Breed: %s}", d.Name, d.Age, d.Breed)
}

// Method with pointer receiver
func (d *Dog) SetBreed(breed string) {
d.Breed = breed
}

// Method with value receiver
func (d Dog) GetBreed() string {
return d.Breed
}

// Create dog instance
dog := Dog{
Animal: Animal{Name: "Buddy", Age: 3},
Breed: "Golden Retriever",
}

// Call promoted methods (from embedded Animal)
fmt.Printf("Dog name: %s\n", dog.GetName())
fmt.Printf("Dog age: %d\n", dog.GetAge())
fmt.Printf("Dog breed: %s\n", dog.GetBreed())
// Output:
// Dog name: Buddy
// Dog age: 3
// Dog breed: Golden Retriever

// Call overridden method
fmt.Printf("Dog: %s\n", dog.String())
// Output: Dog: Dog{Name: Buddy, Age: 3, Breed: Golden Retriever}

// Call promoted pointer receiver methods
dog.SetName("Max")
dog.SetAge(4)
dog.SetBreed("Labrador")

fmt.Printf("After modifications: %s\n", dog.String())
// Output: After modifications: Dog{Name: Max, Age: 4, Breed: Labrador}

// Method promotion with multiple embedding
type WorkingDog struct {
Dog // Embedded struct
Job string
Salary float64
}

// Method with value receiver
func (wd WorkingDog) GetJob() string {
return wd.Job
}

// Method with value receiver
func (wd WorkingDog) GetSalary() float64 {
return wd.Salary
}

// Method with pointer receiver
func (wd *WorkingDog) SetJob(job string) {
wd.Job = job
}

// Method with pointer receiver
func (wd *WorkingDog) SetSalary(salary float64) {
wd.Salary = salary
}

// Create working dog instance
workingDog := WorkingDog{
Dog: Dog{Animal: Animal{Name: "Rex", Age: 5}, Breed: "German Shepherd"},
Job: "Police Dog",
Salary: 50000.0,
}

// Call methods from all levels
fmt.Printf("Working dog name: %s\n", workingDog.GetName()) // From Animal
fmt.Printf("Working dog breed: %s\n", workingDog.GetBreed()) // From Dog
fmt.Printf("Working dog job: %s\n", workingDog.GetJob()) // From WorkingDog
fmt.Printf("Working dog salary: $%.2f\n", workingDog.GetSalary()) // From WorkingDog
// Output:
// Working dog name: Rex
// Working dog breed: German Shepherd
// Working dog job: Police Dog
// Working dog salary: $50000.00

// Method resolution order:
fmt.Println("\nMethod resolution order:")
fmt.Println("1. Methods defined on the type itself")
fmt.Println("2. Methods promoted from embedded types (in order of embedding)")
fmt.Println("3. Methods from deeper embedding levels")
fmt.Println("4. If there are conflicts, the method from the outer type wins")
}

Method Chaining and Fluent Interfaces

Implementing Method Chaining

Method Chaining Pattern

Method chaining allows you to call multiple methods in sequence.

Fluent Interface Design

Fluent interfaces provide a readable way to configure objects.

package main

import "fmt"

func main() {
// Method chaining and fluent interfaces examples
fmt.Println("Method chaining and fluent interfaces examples:")

// Define a struct for method chaining
type QueryBuilder struct {
table string
columns []string
where []string
orderBy string
limit int
}

// Constructor
func NewQueryBuilder() *QueryBuilder {
return &QueryBuilder{
columns: make([]string, 0),
where: make([]string, 0),
limit: -1,
}
}

// Method with pointer receiver that returns the receiver for chaining
func (qb *QueryBuilder) Select(columns ...string) *QueryBuilder {
qb.columns = append(qb.columns, columns...)
return qb
}

// Method with pointer receiver that returns the receiver for chaining
func (qb *QueryBuilder) From(table string) *QueryBuilder {
qb.table = table
return qb
}

// Method with pointer receiver that returns the receiver for chaining
func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
qb.where = append(qb.where, condition)
return qb
}

// Method with pointer receiver that returns the receiver for chaining
func (qb *QueryBuilder) OrderBy(column string) *QueryBuilder {
qb.orderBy = column
return qb
}

// Method with pointer receiver that returns the receiver for chaining
func (qb *QueryBuilder) Limit(count int) *QueryBuilder {
qb.limit = count
return qb
}

// Method that builds the final query
func (qb *QueryBuilder) Build() string {
query := "SELECT "

if len(qb.columns) == 0 {
query += "*"
} else {
for i, column := range qb.columns {
if i > 0 {
query += ", "
}
query += column
}
}

query += " FROM " + qb.table

if len(qb.where) > 0 {
query += " WHERE "
for i, condition := range qb.where {
if i > 0 {
query += " AND "
}
query += condition
}
}

if qb.orderBy != "" {
query += " ORDER BY " + qb.orderBy
}

if qb.limit > 0 {
query += fmt.Sprintf(" LIMIT %d", qb.limit)
}

return query
}

// Use method chaining
query := NewQueryBuilder().
Select("id", "name", "email").
From("users").
Where("age > 18").
Where("status = 'active'").
OrderBy("name").
Limit(10).
Build()

fmt.Printf("Generated query: %s\n", query)
// Output: Generated query: SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name LIMIT 10

// Another example with different chaining
query2 := NewQueryBuilder().
Select("*").
From("products").
Where("price < 100").
OrderBy("price").
Build()

fmt.Printf("Generated query 2: %s\n", query2)
// Output: Generated query 2: SELECT * FROM products WHERE price < 100 ORDER BY price

// Fluent interface for configuration
type Config struct {
host string
port int
database string
username string
password string
ssl bool
}

// Constructor
func NewConfig() *Config {
return &Config{
port: 5432,
ssl: false,
}
}

// Fluent methods
func (c *Config) Host(host string) *Config {
c.host = host
return c
}

func (c *Config) Port(port int) *Config {
c.port = port
return c
}

func (c *Config) Database(db string) *Config {
c.database = db
return c
}

func (c *Config) Credentials(username, password string) *Config {
c.username = username
c.password = password
return c
}

func (c *Config) WithSSL() *Config {
c.ssl = true
return c
}

func (c *Config) String() string {
return fmt.Sprintf("Config{host: %s, port: %d, database: %s, username: %s, ssl: %t}",
c.host, c.port, c.database, c.username, c.ssl)
}

// Use fluent interface
config := NewConfig().
Host("localhost").
Port(5432).
Database("mydb").
Credentials("user", "password").
WithSSL()

fmt.Printf("Configuration: %s\n", config)
// Output: Configuration: Config{host: localhost, port: 5432, database: mydb, username: user, ssl: true}

// Another configuration example
config2 := NewConfig().
Host("remote-server").
Database("production").
Credentials("admin", "secret")

fmt.Printf("Configuration 2: %s\n", config2)
// Output: Configuration 2: Config{host: remote-server, port: 5432, database: production, username: admin, ssl: false}
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's methods and receivers:

Method Definition and Syntax

  • Understanding method definition with receiver syntax
  • Working with value and pointer receivers
  • Implementing methods on custom types
  • Understanding method binding and type association

Value vs Pointer Receivers

  • Understanding the differences between value and pointer receivers
  • Knowing when to use each receiver type
  • Understanding performance implications
  • Following best practices for receiver selection

Method Sets and Promotion

  • Understanding Go's method set rules
  • Working with method promotion through embedding
  • Handling method resolution and conflicts
  • Understanding method availability on types

Method Chaining and Fluent Interfaces

  • Implementing method chaining patterns
  • Creating fluent interfaces for configuration
  • Understanding the benefits of method chaining
  • Following best practices for fluent interface design

Key Concepts

  • func - Defining methods with receiver syntax
  • Value receivers - Methods that receive a copy of the value
  • Pointer receivers - Methods that receive a reference to the value
  • Method sets - Rules that determine method availability
  • Method promotion - Automatic method availability through embedding

Next Steps

You now have a solid foundation in Go's methods and receivers. In the next section, we'll explore struct embedding, which enables composition over inheritance and provides powerful code reuse mechanisms.

Understanding methods and receivers is crucial for implementing object-oriented patterns in Go. These concepts form the foundation for all the more advanced programming techniques we'll cover in the coming chapters.


Ready to learn about struct embedding? Let's explore Go's powerful embedding system and learn how to compose types effectively!