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:

GO
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

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

GO
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

GO
// 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

GO
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!
}
💡 Tip: Method names should be concise and meaningful Method names should be a verb or verb phrase describing the operation performed. Examples: Area(), Scale(), Save(), IsValid().

💡 Tip: Keep receiver names short Receiver variables are typically the first letter or first two letters of the type name. For example, func (r Rectangle) instead of func (rect Rectangle). This is Go convention.

💡 Tip: Consistency principle All methods of the same type should use the same receiver type — either all value receivers or all pointer receivers (when modifications are needed). Don't mix them.

💡 Tip: When to use pointer receivers Always use pointer receivers when the struct contains non-copyable fields (like 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.

GO
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())
}
▶ Try it Yourself

Output:

TEXT
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.

GO
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())
}
▶ Try it Yourself

Output:

TEXT
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.

GO
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
}
▶ Try it Yourself

Output:

TEXT
=== 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.

GO
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:

TEXT
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.

GO
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:

TEXT
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:

  1. Need to modify the receiver's fields → Pointer receiver
  2. Receiver is a large struct (many fields or large arrays) → Pointer receiver (avoids copy overhead)
  3. Struct contains non-copyable fields like sync.Mutex → Must use pointer receiver
  4. Read-only operation and struct is small → Value receiver is fine
  5. When in doubt → Prefer pointer receiver
GO
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.

GO
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.

GO
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.

GO
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:

  1. Method definition: Bind functions to types through receivers
  2. Value receiver vs pointer receiver: Value receivers operate on copies; pointer receivers can modify original values
  3. Method set rules: Value types only have value receiver methods; pointer types have all methods
  4. Composition over inheritance: Achieve method reuse and overriding through embedded structs
  5. 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:

GO
// 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:

GO
// 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:

  1. Define a Shape base interface with Area() float64 and Perimeter() float64 methods
  2. Implement Rectangle, Circle, and Triangle structs
  3. Create a Canvas struct that can hold multiple shapes
  4. Implement Canvas.TotalArea() to calculate the total area of all shapes
  5. Use embedded structs to create ColoredRectangle, which has both Rectangle's methods and a Color field
GO
// 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏