Methods
Lesson 8: Methods
Real-World Analogy
Imagine you bought a car. The car has various functions — start, accelerate, brake, turn on lights. These functions don't exist independently; they belong to the car. You wouldn't say "start" is a generic action; you'd say "the car started."
In Go, a method is a function bound to a specific type. Just as "start" belongs to "car," a method belongs to the type it's bound to. You wouldn't put the start function on a refrigerator — similarly, methods are defined on the type they truly serve.
Core Concepts
What is a Method?
A method is a function with a receiver. The receiver binds the method to a type, allowing variables of that type to call the method directly.
Receiver
The receiver is the bridge between a method and a type. Its syntax appears between the func keyword and the method name:
func (receiverVariable ReceiverType) methodName(parameters) returnType {
// Method body
}
Value Receiver vs Pointer Receiver
| Feature | Value Receiver func (s Struct) |
Pointer Receiver func (s *Struct) |
|---|---|---|
| Operates on copy | Yes (modifications don't affect original) | No (directly modifies original) |
| Use case | Read-only operations, small structs | Need to modify fields, large structs |
| Interface implementation | Callable on both values and pointers | Only callable on pointer types |
Method Set Rules
- Value type method set: only includes value receiver methods
- Pointer type method set: includes both value receiver and pointer receiver methods
This means: if you have a variable of type *T, it can call all methods of both T and *T; while a variable of type T can only call T's methods.
Composition Over Inheritance
Go has no class inheritance; instead, code reuse is achieved through composition (embedding). Embed one struct into another, and the embedded struct's methods are automatically "promoted" to the outer struct.
Basic Syntax and Usage
Defining Methods
package main
import "fmt"
// Define a struct
type Rectangle struct {
Width float64
Height float64
}
// Value receiver method: calculate area
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Pointer receiver method: scale (needs to modify fields)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("Area:", rect.Area()) // Output: Area: 50
rect.Scale(2)
fmt.Println("Scaled area:", rect.Area()) // Output: Scaled area: 200
}
Difference Between Methods and Functions
// This is a function, not bound to any type
func Area(r Rectangle) float64 {
return r.Width * r.Height
}
// This is a method, bound to the Rectangle type
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
Method Inheritance with Embedded Structs
package main
import "fmt"
// Base type
type Animal struct {
Name string
}
// Animal's method
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
// Type that embeds Animal
type Dog struct {
Animal // Embed, no field name needed
Breed string
}
// Dog's own method
func (d Dog) Bark() string {
return d.Name + " barks!"
}
func main() {
dog := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Golden Retriever",
}
// Can directly call the embedded type's methods
fmt.Println(dog.Speak()) // Output: Rex makes a sound
fmt.Println(dog.Bark()) // Output: Rex barks!
}
Area(), Scale(), Save(), IsValid().
func (r Rectangle) instead of func (rect Rectangle). This is Go convention.
sync.Mutex) or when the struct is large.
Examples
Example: Basic Method Definition (Difficulty ⭐)
Define a Circle struct and implement methods for calculating area and perimeter.
package main
import (
"fmt"
"math"
)
// Circle struct
type Circle struct {
Radius float64
}
// Area calculates area (value receiver, read-only operation)
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Perimeter calculates perimeter
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// SetRadius sets a new radius (pointer receiver, modifies field)
func (c *Circle) SetRadius(r float64) {
if r > 0 {
c.Radius = r
}
}
func main() {
c := Circle{Radius: 5}
fmt.Printf("Radius: %.2f\n", c.Radius)
fmt.Printf("Area: %.2f\n", c.Area())
fmt.Printf("Perimeter: %.2f\n", c.Perimeter())
c.SetRadius(10)
fmt.Printf("\nNew radius: %.2f\n", c.Radius)
fmt.Printf("New area: %.2f\n", c.Area())
fmt.Printf("New perimeter: %.2f\n", c.Perimeter())
}
Output:
Radius: 5.00
Area: 78.54
Perimeter: 31.42
New radius: 10.00
New area: 314.16
New perimeter: 62.83
Example: Value Receiver vs Pointer Receiver (Difficulty ⭐⭐)
Demonstrates the key behavioral differences between the two.
package main
import "fmt"
// Account bank account
type Account struct {
Owner string
Balance float64
}
// Value receiver: get balance (read-only, no modification)
func (a Account) GetBalance() float64 {
return a.Balance
}
// Pointer receiver: deposit (needs to modify balance)
func (a *Account) Deposit(amount float64) {
if amount > 0 {
a.Balance += amount
fmt.Printf(" Deposited %.2f, Balance: %.2f\n", amount, a.Balance)
}
}
// Pointer receiver: withdraw
func (a *Account) Withdraw(amount float64) bool {
if amount > 0 && a.Balance >= amount {
a.Balance -= amount
fmt.Printf(" Withdrew %.2f, Balance: %.2f\n", amount, a.Balance)
return true
}
fmt.Printf(" Withdrawal of %.2f failed, insufficient balance\n", amount)
return false
}
// Value receiver: format account info
func (a Account) String() string {
return fmt.Sprintf("Account[%s] Balance: %.2f", a.Owner, a.Balance)
}
func main() {
acc := Account{Owner: "Zhang San", Balance: 1000}
fmt.Println(acc)
fmt.Println("\n--- Transaction History ---")
acc.Deposit(500)
acc.Withdraw(200)
acc.Withdraw(2000) // Insufficient balance
fmt.Println("\n--- Final Status ---")
fmt.Println(acc)
fmt.Printf("Current balance: %.2f\n", acc.GetBalance())
}
Output:
Account[Zhang San] Balance: 1000.00
--- Transaction History ---
Deposited 500.00, Balance: 1500.00
Withdrew 200.00, Balance: 1300.00
Withdrawal of 2000.00 failed, insufficient balance
--- Final Status ---
Account[Zhang San] Balance: 1300.00
Current balance: 1300.00
Example: Composition Over Inheritance (Difficulty ⭐⭐⭐)
Demonstrate method inheritance through embedded structs and method overriding.
package main
import "fmt"
// ==================== Base Layer ====================
// Base struct (similar to a "parent class")
type Base struct {
ID int
Name string
}
// Describe returns a basic description
func (b Base) Describe() string {
return fmt.Sprintf("Base[ID=%d, Name=%s]", b.ID, b.Name)
}
// Identify returns identification info
func (b Base) Identify() string {
return fmt.Sprintf("I am %s (ID: %d)", b.Name, b.ID)
}
// ==================== Business Layer ====================
// Employee embeds Base
type Employee struct {
Base // Embed, inherits Base's methods
Department string
Salary float64
}
// Override Base's Describe method
func (e Employee) Describe() string {
return fmt.Sprintf("Employee[ID=%d, Name=%s, Dept=%s, Salary=%.0f]",
e.ID, e.Name, e.Department, e.Salary)
}
// Employee's own method
func (e Employee) AnnualSalary() float64 {
return e.Salary * 12
}
// Manager embeds Employee (multi-level embedding)
type Manager struct {
Employee // Embed Employee
TeamSize int
}
// Override Describe method
func (m Manager) Describe() string {
return fmt.Sprintf("Manager[ID=%d, Name=%s, Dept=%s, Team=%d people]",
m.ID, m.Name, m.Department, m.TeamSize)
}
// Manager's own method
func (m Manager) TeamReport() string {
return fmt.Sprintf("%s manages a team of %d people", m.Name, m.TeamSize)
}
func main() {
// Create a Base instance
b := Base{ID: 1, Name: "Base Object"}
fmt.Println("=== Base ===")
fmt.Println(b.Describe())
fmt.Println(b.Identify())
// Create an Employee instance
emp := Employee{
Base: Base{ID: 2, Name: "Li Si"},
Department: "Engineering",
Salary: 15000,
}
fmt.Println("\n=== Employee ===")
fmt.Println(emp.Describe()) // Calls Employee's overridden method
fmt.Println(emp.Identify()) // Inherited from Base
fmt.Printf("Annual salary: %.0f\n", emp.AnnualSalary())
// Create a Manager instance
mgr := Manager{
Employee: Employee{
Base: Base{ID: 3, Name: "Wang Wu"},
Department: "R&D",
Salary: 25000,
},
TeamSize: 8,
}
fmt.Println("\n=== Manager ===")
fmt.Println(mgr.Describe()) // Calls Manager's overridden method
fmt.Println(mgr.Identify()) // Inherited from Base through multi-level embedding
fmt.Printf("Annual salary: %.0f\n", mgr.AnnualSalary()) // Inherited from Employee
fmt.Println(mgr.TeamReport()) // Manager's own method
}
Output:
=== Base ===
Base[ID=1, Name=Base Object]
I am Base Object (ID: 1)
=== Employee ===
Employee[ID=2, Name=Li Si, Dept=Engineering, Salary=15000]
I am Li Si (ID: 2)
Annual salary: 180000
=== Manager ===
Manager[ID=3, Name=Wang Wu, Dept=R&D, Team=8 people]
I am Wang Wu (ID: 3)
Annual salary: 300000
Wang Wu manages a team of 8 people
Real-World Scenarios
Scenario 1: User Authentication System
Using methods to encapsulate user authentication logic.
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
// User struct
type User struct {
Username string
PasswordHash string
Email string
CreatedAt time.Time
IsActive bool
LoginCount int
}
// NewUser creates a new user (constructor pattern)
func NewUser(username, password, email string) *User {
return &User{
Username: username,
PasswordHash: hashPassword(password),
Email: email,
CreatedAt: time.Now(),
IsActive: true,
LoginCount: 0,
}
}
// hashPassword hashes a password (package-level function, not a method)
func hashPassword(password string) string {
h := sha256.Sum256([]byte(password))
return hex.EncodeToString(h[:])
}
// VerifyPassword verifies the password
func (u *User) VerifyPassword(password string) bool {
return u.PasswordHash == hashPassword(password)
}
// Login user login
func (u *User) Login(password string) error {
if !u.IsActive {
return fmt.Errorf("account %s has been disabled", u.Username)
}
if !u.VerifyPassword(password) {
return fmt.Errorf("incorrect password")
}
u.LoginCount++
return nil
}
// Deactivate disables the account
func (u *User) Deactivate() {
u.IsActive = false
}
// Info returns user information
func (u User) Info() string {
status := "Active"
if !u.IsActive {
status = "Disabled"
}
return fmt.Sprintf("[%s] %s | Email: %s | Status: %s | Login count: %d",
u.Username, u.Username, u.Email, status, u.LoginCount)
}
func main() {
user := NewUser("zhangsan", "mySecret123", "zhangsan@example.com")
fmt.Println("Created user:")
fmt.Println(user.Info())
// Login with correct password
fmt.Println("\nLogin with correct password:")
err := user.Login("mySecret123")
if err != nil {
fmt.Println("Login failed:", err)
} else {
fmt.Println("Login successful!")
}
fmt.Println(user.Info())
// Login with wrong password
fmt.Println("\nLogin with wrong password:")
err = user.Login("wrongPassword")
if err != nil {
fmt.Println("Login failed:", err)
}
// Try to login after disabling account
fmt.Println("\nTry login after disabling account:")
user.Deactivate()
err = user.Login("mySecret123")
if err != nil {
fmt.Println("Login failed:", err)
}
fmt.Println(user.Info())
}
Sample Output:
Created user:
[zhangsan] zhangsan | Email: zhangsan@example.com | Status: Active | Login count: 0
Login with correct password:
Login successful!
[zhangsan] zhangsan | Email: zhangsan@example.com | Status: Active | Login count: 1
Login with wrong password:
Login failed: incorrect password
Try login after disabling account:
Login failed: account zhangsan has been disabled
[zhangsan] zhangsan | Email: zhangsan@example.com | Status: Disabled | Login count: 1
Scenario 2: Shopping Cart System
Using methods to implement shopping cart CRUD and checkout.
package main
import "fmt"
// Product product
type Product struct {
Name string
Price float64
Category string
}
// CartItem cart item
type CartItem struct {
Product Product
Quantity int
}
// Subtotal calculates the subtotal
func (ci CartItem) Subtotal() float64 {
return ci.Product.Price * float64(ci.Quantity)
}
// ShoppingCart shopping cart
type ShoppingCart struct {
Items []CartItem
Owner string
}
// Add adds a product to the cart
func (sc *ShoppingCart) Add(p Product, qty int) {
// Check if already exists
for i := range sc.Items {
if sc.Items[i].Product.Name == p.Name {
sc.Items[i].Quantity += qty
fmt.Printf(" Updated quantity: %s x%d\n", p.Name, sc.Items[i].Quantity)
return
}
}
sc.Items = append(sc.Items, CartItem{Product: p, Quantity: qty})
fmt.Printf(" Added product: %s x%d\n", p.Name, qty)
}
// Remove removes a product from the cart
func (sc *ShoppingCart) Remove(name string) bool {
for i, item := range sc.Items {
if item.Product.Name == name {
sc.Items = append(sc.Items[:i], sc.Items[i+1:]...)
fmt.Printf(" Removed product: %s\n", name)
return true
}
}
fmt.Printf(" Product not found: %s\n", name)
return false
}
// Total calculates the total price
func (sc ShoppingCart) Total() float64 {
var total float64
for _, item := range sc.Items {
total += item.Subtotal()
}
return total
}
// Count returns the number of product types
func (sc ShoppingCart) Count() int {
return len(sc.Items)
}
// Display prints the cart contents
func (sc ShoppingCart) Display() {
fmt.Printf("\n🛒 %s's cart (%d products):\n", sc.Owner, sc.Count())
fmt.Println(" ─────────────────────────────────────")
for _, item := range sc.Items {
fmt.Printf(" %-12s $%.2f x %d = $%.2f\n",
item.Product.Name, item.Product.Price, item.Quantity, item.Subtotal())
}
fmt.Println(" ─────────────────────────────────────")
fmt.Printf(" Total: $%.2f\n", sc.Total())
}
func main() {
cart := ShoppingCart{Owner: "Zhang San"}
// Define products
laptop := Product{Name: "Laptop", Price: 999, Category: "Electronics"}
mouse := Product{Name: "Wireless Mouse", Price: 29, Category: "Electronics"}
book := Product{Name: "Go Programming", Price: 45, Category: "Books"}
coffee := Product{Name: "Coffee", Price: 5, Category: "Food"}
// Add products
fmt.Println("Adding products:")
cart.Add(laptop, 1)
cart.Add(mouse, 2)
cart.Add(book, 3)
cart.Add(coffee, 5)
cart.Display()
// Update quantities
fmt.Println("\nUpdating quantities:")
cart.Add(mouse, 1) // Add 1 more mouse
cart.Add(book, -1) // Remove 1 book (via negative number)
cart.Display()
// Remove product
fmt.Println("\nRemoving product:")
cart.Remove("Coffee")
cart.Display()
}
Output:
Adding products:
Added product: Laptop x1
Added product: Wireless Mouse x2
Added product: Go Programming x3
Added product: Coffee x5
🛒 Zhang San's cart (4 products):
─────────────────────────────────────
Laptop $999.00 x 1 = $999.00
Wireless Mouse $29.00 x 2 = $58.00
Go Programming $45.00 x 3 = $135.00
Coffee $5.00 x 5 = $25.00
─────────────────────────────────────
Total: $1217.00
Updating quantities:
Updated quantity: Wireless Mouse x3
Updated quantity: Go Programming x2
🛒 Zhang San's cart (4 products):
─────────────────────────────────────
Laptop $999.00 x 1 = $999.00
Wireless Mouse $29.00 x 3 = $87.00
Go Programming $45.00 x 2 = $90.00
Coffee $5.00 x 5 = $25.00
─────────────────────────────────────
Total: $1201.00
Removing product:
Removed product: Coffee
🛒 Zhang San's cart (3 products):
─────────────────────────────────────
Laptop $999.00 x 1 = $999.00
Wireless Mouse $29.00 x 3 = $87.00
Go Programming $45.00 x 2 = $90.00
─────────────────────────────────────
Total: $1176.00
❓ FAQ
Q1: How do I choose between value receiver and pointer receiver?
Follow these rules:
- Need to modify the receiver's fields → Pointer receiver
- Receiver is a large struct (many fields or large arrays) → Pointer receiver (avoids copy overhead)
- Struct contains non-copyable fields like
sync.Mutex→ Must use pointer receiver - Read-only operation and struct is small → Value receiver is fine
- When in doubt → Prefer pointer receiver
type Small struct { X int }
func (s Small) Get() int { return s.X } // ✅ Value receiver, read-only
type Large struct { Data [1024]byte }
func (l *Large) Process() { /* ... */ } // ✅ Pointer receiver, avoids copy
type Safe struct { mu sync.Mutex }
func (s *Safe) Lock() { s.mu.Lock() } // ✅ Must use pointer, Mutex is non-copyable
Q2: Why does the compiler give an error when I call a method?
The most common reason is confusing value receiver and pointer receiver calling conventions.
type Dog struct { Name string }
func (d *Dog) Rename(name string) {
d.Name = name
}
func main() {
d := Dog{Name: "Rex"}
d.Rename("Buddy") // ✅ Go auto-takes address, equivalent to (&d).Rename("Buddy")
// But this will cause an error:
// Dog{"Rex"}.Rename("Buddy") // ❌ Cannot take address of non-addressable value
}
The Go compiler automatically handles the d.Rename() → (&d).Rename() conversion, but only if the variable is addressable. Literals and temporary values are not addressable.
Q3: Can methods return multiple values?
Yes, methods and functions have identical signature rules and can have any number of parameters and return values.
type Calculator struct {
Value float64
}
// Returns quotient and remainder
func (c Calculator) DivideBy(divisor float64) (float64, float64, error) {
if divisor == 0 {
return 0, 0, fmt.Errorf("divisor cannot be zero")
}
return c.Value / divisor, math.Mod(c.Value, divisor), nil
}
Q4: What about method conflicts with embedded structs?
When two embedded types have methods with the same name, the Go compiler will report an error, and you need to explicitly specify which one to call.
type A struct{}
func (A) Hello() string { return "A" }
type B struct{}
func (B) Hello() string { return "B" }
type C struct {
A
B
}
func main() {
c := C{}
// c.Hello() // ❌ Compile error: ambiguous selector c.Hello
fmt.Println(c.A.Hello()) // ✅ Explicit: outputs "A"
fmt.Println(c.B.Hello()) // ✅ Explicit: outputs "B"
}
📖 Summary
In this lesson we learned about methods in Go:
- Method definition: Bind functions to types through receivers
- Value receiver vs pointer receiver: Value receivers operate on copies; pointer receivers can modify original values
- Method set rules: Value types only have value receiver methods; pointer types have all methods
- Composition over inheritance: Achieve method reuse and overriding through embedded structs
- Best practices: Keep receiver names short, use consistent receiver types for the same type, use pointers when modifications are needed
Methods are the foundation of object-oriented programming in Go. The next lesson will cover interfaces — Go's powerful tool for polymorphism, which works closely with methods to form the core of Go's type system.
📝 Exercises
Exercise 1: Temperature Converter ⭐
Create a Temperature struct with a Celsius field, and implement the following methods:
To Fahrenheit()— Convert to Fahrenheit (F = C × 9/5 + 32)To Kelvin()— Convert to Kelvin (K = C + 273.15)Describe() string— Return a formatted temperature description
// Expected output:
// Temperature: 100.00°C = 212.00°F = 373.15K
Exercise 2: Bank Account (with Transaction History) ⭐⭐
Extend the Account struct from Scenario 2 with the following features:
- Transaction history (a slice storing each transaction's amount and time)
History()method to print all transaction recordsStatement()method to generate a formatted account statement
// Expected output:
// === Account Statement ===
// Account: Zhang San
// Transaction history:
// 1. +1000.00 (Opening deposit)
// 2. -200.00 (Purchase)
// 3. +500.00 (Salary)
// Current balance: 1300.00
Exercise 3: Shape System (Composition Inheritance) ⭐⭐⭐
Create a shape system:
- Define a
Shapebase interface withArea() float64andPerimeter() float64methods - Implement
Rectangle,Circle, andTrianglestructs - Create a
Canvasstruct that can hold multiple shapes - Implement
Canvas.TotalArea()to calculate the total area of all shapes - Use embedded structs to create
ColoredRectangle, which has both Rectangle's methods and aColorfield
// Expected output:
// Canvas has 3 shapes
// Total area: xxx.xx
// Red rectangle: width=10, height=5, color=red
Next Lesson
Lesson 9: Interfaces — Learn about Go's interface system: implicit implementation, empty interface, type assertions, interface composition, and Go's most powerful polymorphism mechanism.



