Skip to main content

Methods

Go methods are functions attached to a specific type. Without classes, methods are the key mechanism for adding behavior to user-defined types like structs. Understanding the difference between value receivers and pointer receivers is essential to Go development.

Basic Method Syntax​

A method is a function with a receiver added before the function name in the form (variableName Type).

package main

import (
"fmt"
"math"
)

type Circle struct {
Radius float64
}

// Method on Circle type β€” value receiver
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}

func (c Circle) String() string {
return fmt.Sprintf("Circle(r=%.2f)", c.Radius)
}

type Rectangle struct {
Width, Height float64
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

func main() {
c := Circle{Radius: 5}
fmt.Println(c) // Circle(r=5.00)
fmt.Printf("Area: %.2f\n", c.Area()) // 78.54
fmt.Printf("Perimeter: %.2f\n", c.Perimeter()) // 31.42

rect := Rectangle{Width: 4, Height: 6}
fmt.Printf("Rect area: %.2f\n", rect.Area()) // 24.00
fmt.Printf("Rect perimeter: %.2f\n", rect.Perimeter()) // 20.00
}

Value Receiver vs Pointer Receiver​

This is the most important concept in Go methods.

Value Receiver​

  • Receives a copy of the type
  • Cannot modify the original data
  • Suitable for small structs or read-only methods

Pointer Receiver​

  • Receives a pointer to the type
  • Can modify the original data
  • Reduces copy cost for large structs
  • Required for state-mutating methods
package main

import "fmt"

type Counter struct {
count int
}

// Value receiver: does not modify original
func (c Counter) Value() int {
return c.count
}

// Pointer receiver: modifies original
func (c *Counter) Increment() {
c.count++
}

func (c *Counter) Decrement() {
c.count--
}

func (c *Counter) Reset() {
c.count = 0
}

func main() {
c := Counter{}

c.Increment()
c.Increment()
c.Increment()
fmt.Println(c.Value()) // 3

c.Decrement()
fmt.Println(c.Value()) // 2

c.Reset()
fmt.Println(c.Value()) // 0

// Works the same with pointer declaration
cp := &Counter{}
cp.Increment()
cp.Increment()
fmt.Println(cp.Value()) // 2
}

Automatic Dereferencing and Address Taking​

Go automatically converts between values and pointers when calling methods.

package main

import "fmt"

type Point struct {
X, Y float64
}

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

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

func main() {
// Calling pointer receiver method on value variable
p := Point{X: 3, Y: 4}
p.Scale(2) // Go automatically converts to (&p).Scale(2)
fmt.Println(p) // (6, 8)

// Calling value receiver method on pointer variable
pp := &Point{X: 1, Y: 2}
fmt.Println(pp.String()) // Go automatically converts to (*pp).String()
fmt.Println(pp) // (1, 2)

// Auto-conversion doesn't work on non-addressable values (temporaries)
// Point{X: 1, Y: 2}.Scale(2) // compile error!
}

Method Set​

A method set defines which methods a type has. This is directly related to interface implementation.

TypeAvailable Methods
T (value type)Value receiver methods only
*T (pointer type)Both value and pointer receiver methods
package main

import "fmt"

type Animal struct {
Name string
}

// Value receiver method
func (a Animal) Speak() string {
return a.Name + " says hello"
}

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

func main() {
// Value type: only value receiver directly (Go auto-converts for convenience)
dog := Animal{Name: "Rex"}
fmt.Println(dog.Speak()) // Rex says hello
dog.Rename("Max") // Go converts to (&dog).Rename("Max")
fmt.Println(dog.Name) // Max

// Pointer type: all methods available
cat := &Animal{Name: "Whiskers"}
fmt.Println(cat.Speak()) // Whiskers says hello
cat.Rename("Luna")
fmt.Println(cat.Name) // Luna
}

Adding Methods to Various Types​

You can add methods to any type in the same package, not just structs.

package main

import (
"fmt"
"strings"
)

// Cannot add methods directly to built-in types β€” need new type definition
type Celsius float64
type Fahrenheit float64

func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}

func (f Fahrenheit) ToCelsius() Celsius {
return Celsius((f - 32) * 5 / 9)
}

func (c Celsius) String() string {
return fmt.Sprintf("%.1fΒ°C", float64(c))
}

func (f Fahrenheit) String() string {
return fmt.Sprintf("%.1fΒ°F", float64(f))
}

// Adding methods to a slice type
type StringSlice []string

func (ss StringSlice) Join(sep string) string {
return strings.Join(ss, sep)
}

func (ss StringSlice) Contains(s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}

func (ss *StringSlice) Append(s string) {
*ss = append(*ss, s)
}

func main() {
boiling := Celsius(100)
fmt.Println(boiling) // 100.0Β°C
fmt.Println(boiling.ToFahrenheit()) // 212.0Β°F

bodyTemp := Fahrenheit(98.6)
fmt.Println(bodyTemp.ToCelsius()) // 37.0Β°C

fruits := StringSlice{"apple", "banana", "cherry"}
fmt.Println(fruits.Join(", ")) // apple, banana, cherry
fmt.Println(fruits.Contains("banana")) // true
fruits.Append("date")
fmt.Println(fruits.Join(", ")) // apple, banana, cherry, date
}

Method Expressions​

There are two ways to use methods as function values.

package main

import "fmt"

type Calculator struct {
result float64
}

func (c *Calculator) Add(n float64) *Calculator {
c.result += n
return c
}

func (c *Calculator) Multiply(n float64) *Calculator {
c.result *= n
return c
}

func (c Calculator) Result() float64 {
return c.result
}

func main() {
// Method Value: method bound to a specific instance
calc := &Calculator{result: 10}
addFn := calc.Add // Add method bound to calc
addFn(5) // same as calc.Add(5)
addFn(3) // same as calc.Add(3)
fmt.Println(calc.Result()) // 18

// Method Expression: extract method from type as function
// first argument is the receiver
addExpr := (*Calculator).Add
calc2 := &Calculator{result: 0}
addExpr(calc2, 10) // same as calc2.Add(10)
addExpr(calc2, 20)
fmt.Println(calc2.Result()) // 30

// Functional programming pattern
ops := []func(*Calculator) *Calculator{
func(c *Calculator) *Calculator { return c.Add(1) },
func(c *Calculator) *Calculator { return c.Multiply(3) },
func(c *Calculator) *Calculator { return c.Add(5) },
}

calc3 := &Calculator{result: 2}
for _, op := range ops {
op(calc3)
}
fmt.Println(calc3.Result()) // (2+1)*3+5 = 14
}

Method Chaining​

When a method returns the receiver, you can write in a chaining style.

package main

import (
"fmt"
"strings"
)

type QueryBuilder struct {
table string
conditions []string
orderBy string
limit int
}

func NewQuery(table string) *QueryBuilder {
return &QueryBuilder{table: table, limit: -1}
}

func (q *QueryBuilder) Where(condition string) *QueryBuilder {
q.conditions = append(q.conditions, condition)
return q
}

func (q *QueryBuilder) OrderBy(field string) *QueryBuilder {
q.orderBy = field
return q
}

func (q *QueryBuilder) Limit(n int) *QueryBuilder {
q.limit = n
return q
}

func (q *QueryBuilder) Build() string {
query := fmt.Sprintf("SELECT * FROM %s", q.table)
if len(q.conditions) > 0 {
query += " WHERE " + strings.Join(q.conditions, " AND ")
}
if q.orderBy != "" {
query += " ORDER BY " + q.orderBy
}
if q.limit > 0 {
query += fmt.Sprintf(" LIMIT %d", q.limit)
}
return query
}

func main() {
// Build query with method chaining
query := NewQuery("users").
Where("age > 18").
Where("active = true").
OrderBy("created_at DESC").
Limit(10).
Build()

fmt.Println(query)
// SELECT * FROM users WHERE age > 18 AND active = true ORDER BY created_at DESC LIMIT 10
}

Method Reuse via Embedding​

Go uses struct embedding instead of class inheritance to reuse methods.

package main

import "fmt"

type Base struct {
ID int
Name string
}

func (b Base) Describe() string {
return fmt.Sprintf("ID: %d, Name: %s", b.ID, b.Name)
}

func (b *Base) SetName(name string) {
b.Name = name
}

type Employee struct {
Base // Embed Base β€” all Base methods are available
Department string
Salary float64
}

func (e Employee) Describe() string {
// Explicitly call Base's Describe
return fmt.Sprintf("%s, Department: %s", e.Base.Describe(), e.Department)
}

type Manager struct {
Employee // Embed Employee
Reports []*Employee
}

func (m *Manager) AddReport(e *Employee) {
m.Reports = append(m.Reports, e)
}

func main() {
emp := Employee{
Base: Base{ID: 1, Name: "Alice"},
Department: "Engineering",
Salary: 75000,
}

fmt.Println(emp.Describe()) // ID: 1, Name: Alice, Department: Engineering
emp.SetName("Alicia") // Call Base's method directly
fmt.Println(emp.Name) // Alicia
fmt.Println(emp.Base.Describe()) // ID: 1, Name: Alicia

mgr := Manager{
Employee: Employee{
Base: Base{ID: 2, Name: "Bob"},
Department: "Engineering",
},
}
mgr.AddReport(&emp)
fmt.Printf("%s's team size: %d\n", mgr.Name, len(mgr.Reports))
}

Practical Example: Bank Account System​

package main

import (
"errors"
"fmt"
"time"
)

type Transaction struct {
Type string
Amount float64
Timestamp time.Time
}

type BankAccount struct {
owner string
balance float64
transactions []Transaction
}

func NewBankAccount(owner string, initialBalance float64) (*BankAccount, error) {
if initialBalance < 0 {
return nil, errors.New("initial balance must be non-negative")
}
return &BankAccount{
owner: owner,
balance: initialBalance,
}, nil
}

func (a *BankAccount) Deposit(amount float64) error {
if amount <= 0 {
return errors.New("deposit amount must be positive")
}
a.balance += amount
a.transactions = append(a.transactions, Transaction{
Type: "Deposit",
Amount: amount,
Timestamp: time.Now(),
})
return nil
}

func (a *BankAccount) Withdraw(amount float64) error {
if amount <= 0 {
return errors.New("withdrawal amount must be positive")
}
if amount > a.balance {
return fmt.Errorf("insufficient balance: have %.2f, want %.2f", a.balance, amount)
}
a.balance -= amount
a.transactions = append(a.transactions, Transaction{
Type: "Withdrawal",
Amount: amount,
Timestamp: time.Now(),
})
return nil
}

func (a BankAccount) Balance() float64 {
return a.balance
}

func (a BankAccount) Owner() string {
return a.owner
}

func (a BankAccount) PrintStatement() {
fmt.Printf("=== Account Statement for %s ===\n", a.owner)
for _, t := range a.transactions {
fmt.Printf("[%s] %s: %.2f\n",
t.Timestamp.Format("15:04:05"), t.Type, t.Amount)
}
fmt.Printf("Current balance: %.2f\n", a.balance)
}

func main() {
account, err := NewBankAccount("John Doe", 10000)
if err != nil {
fmt.Println("Error:", err)
return
}

account.Deposit(5000)
account.Deposit(3000)
account.Withdraw(2000)

if err := account.Withdraw(20000); err != nil {
fmt.Println("Withdrawal failed:", err)
}

account.PrintStatement()
}

Key Summary

  • Method = function with a receiver; the receiver is the type the method belongs to
  • Value receiver: works on copy (read-only), Pointer receiver: modifies original
  • Convention: don't mix value and pointer receivers on the same type
  • Go auto-converts between value/pointer, but be careful with interface implementation
  • Struct embedding reuses methods (replaces inheritance)