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.
| Type | Available 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)