Structs and Methods

The Foundation of Go's Object System

Structs and methods form the foundation of Go's approach to object-oriented programming. Unlike traditional OOP languages with classes and inheritance, Go uses a simpler but more powerful composition-based approach.

Real-world impact: Think about building a user management system. You need to organize user data and behaviors. Go's structs and methods provide a clean, efficient way to model this relationship between data and behavior without the complexity of inheritance hierarchies.

Business value: Structs with methods enable you to:

  • Model domain entities like users, products, and transactions
  • Organize related data into cohesive, type-safe structures
  • Add behavior that operates directly on your data
  • Build maintainable APIs that clearly express intent
  • Implement interfaces for polymorphism without inheritance complexity

Performance advantage: Go's structs are value types with efficient memory layout, making them perfect for high-performance systems where memory usage matters.

Learning Objectives

By the end of this tutorial, you will be able to:

  • Design and create structs with proper field organization
  • Implement methods with both value and pointer receivers
  • Understand when to use each receiver type for optimal performance
  • Master composition over inheritance for flexible code reuse
  • Create constructor functions for robust object initialization
  • Apply struct tags for serialization and metadata
  • Avoid common pitfalls like race conditions and memory leaks

Understanding Go's Object Model

The Philosophy: Composition Over Inheritance

Go deliberately avoids traditional class inheritance. Instead, it promotes composition - building complex types by combining simpler ones.

Why this matters: Traditional inheritance creates tight coupling and fragile base classes. Go's composition creates:

  • Loose coupling between components
  • Clear relationships
  • Flexible code reuse without inheritance constraints
  • Explicit dependencies that are easy to test
1// Traditional inheritance:
2// class Employee extends Person  // Tight coupling, fragile
3
4// Go's composition:
5type Employee struct {
6    Person    // Employee HAS-A Person
7    Salary int
8}

Structs: Typed Data Aggregates

A struct is a collection of named fields, each with a specific type. Think of it as a blueprint that defines what data belongs together.

Key characteristics:

  • Value type: Structs are copied when assigned
  • Fixed layout: Fields are arranged in memory for efficient access
  • Type safety: Compile-time checking prevents field errors
  • Zero values: Each field gets a meaningful default value

Memory efficiency: Unlike languages where objects are always heap-allocated, Go's structs can be stack-allocated for better performance.

Methods: Behavior Bound to Types

Methods are functions with a special receiver parameter that binds them to a type. This creates the association between data and behavior.

Two receiver types:

  • Value receivers: Work on a copy of the struct
  • Pointer receivers: Work on the original struct

Automatic dereferencing: Go automatically handles pointer-to-value and value-to-pointer conversions, making method calls consistent regardless of receiver type.

From Basics to Mastery

Example 1: Basic Struct Definition and Usage

Let's start with a simple but practical example - a user profile system:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9// Step 1: Define the struct with meaningful field names
10type User struct {
11    ID        int       // Unique identifier
12    Name      string    // User's display name
13    Email     string    // Contact information
14    CreatedAt time.Time // When user joined
15    LastLogin time.Time // Last activity timestamp
16}
17
18// Step 2: Add constructor for proper initialization
19func NewUser(id int, name, email string) *User {
20    return &User{
21        ID:        id,
22        Name:      name,
23        Email:     email,
24        CreatedAt: time.Now(),
25        LastLogin: time.Time{}, // Zero value indicates no login yet
26    }
27}
28
29// Step 3: Add methods with appropriate receiver types
30func (u User) String() string {
31    // Value receiver - only reads data
32    return fmt.Sprintf("User{id:%d, name:%s, email:%s}", u.ID, u.Name, u.Email)
33}
34
35func (u *User) Login() {
36    // Pointer receiver - modifies the user
37    u.LastLogin = time.Now()
38    fmt.Printf("User %s logged in at %v\n", u.Name, u.LastLogin)
39}
40
41func (u User) IsActive() bool {
42    // Value receiver - read-only operation
43    return !u.LastLogin.IsZero()
44}
45
46func main() {
47    // Create a user using constructor
48    user := NewUser(1, "Alice Johnson", "alice@example.com")
49
50    fmt.Println(user)           // Uses String() method
51    fmt.Println(user.IsActive()) // User is not active yet
52
53    user.Login()               // Modifies the user
54    fmt.Println(user.IsActive()) // Now user is active
55}

What this demonstrates:

  • Struct fields organize related data logically
  • Constructor functions ensure proper initialization
  • Value receivers are for read-only operations
  • Pointer receivers are for modifying operations
  • Methods create clean, self-documenting APIs

Example 2: Progressive Method Implementation

Let's build a more complex example that shows method evolution:

  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "strings"
  7)
  8
  9type Product struct {
 10    ID          string
 11    Name        string
 12    Price       float64
 13    Stock       int
 14    Description string
 15}
 16
 17// Constructor with validation
 18func NewProduct(id, name string, price float64, stock int) (*Product, error) {
 19    if id == "" {
 20        return nil, fmt.Errorf("product ID cannot be empty")
 21    }
 22    if name == "" {
 23        return nil, fmt.Errorf("product name cannot be empty")
 24    }
 25    if price < 0 {
 26        return nil, fmt.Errorf("product price cannot be negative")
 27    }
 28    if stock < 0 {
 29        return nil, fmt.Errorf("product stock cannot be negative")
 30    }
 31
 32    return &Product{
 33        ID:          id,
 34        Name:        name,
 35        Price:       price,
 36        Stock:       stock,
 37        Description: "", // Can be set later
 38    }, nil
 39}
 40
 41// Read-only operations use value receivers
 42func (p Product) IsAvailable() bool {
 43    return p.Stock > 0
 44}
 45
 46func (p Product) IsInStock(quantity int) bool {
 47    return p.Stock >= quantity
 48}
 49
 50func (p Product) TotalValue() float64 {
 51    return p.Price * float64(p.Stock)
 52}
 53
 54// Modifying operations use pointer receivers
 55func (p *Product) SetDescription(desc string) {
 56    p.Description = strings.TrimSpace(desc)
 57}
 58
 59func (p *Product) UpdatePrice(newPrice float64) error {
 60    if newPrice < 0 {
 61        return fmt.Errorf("price cannot be negative")
 62    }
 63    p.Price = newPrice
 64    return nil
 65}
 66
 67func (p *Product) AddStock(quantity int) error {
 68    if quantity < 0 {
 69        return fmt.Errorf("quantity to add cannot be negative")
 70    }
 71    p.Stock += quantity
 72    return nil
 73}
 74
 75func (p *Product) ReduceStock(quantity int) error {
 76    if quantity < 0 {
 77        return fmt.Errorf("quantity to reduce cannot be negative")
 78    }
 79    if p.Stock < quantity {
 80        return fmt.Errorf("insufficient stock: have %d, need %d", p.Stock, quantity)
 81    }
 82    p.Stock -= quantity
 83    return nil
 84}
 85
 86// String representation for debugging
 87func (p Product) String() string {
 88    return fmt.Sprintf("Product{id:%s, name:%s, price:%.2f, stock:%d}",
 89        p.ID, p.Name, p.Price, p.Stock)
 90}
 91
 92func main() {
 93    // Create product with constructor validation
 94    product, err := NewProduct("PROD-001", "Laptop", 999.99, 10)
 95    if err != nil {
 96        fmt.Println("Error creating product:", err)
 97        return
 98    }
 99
100    // Use read-only methods
101    fmt.Println("Product available:", product.IsAvailable())
102    fmt.Println("Can sell 5 units:", product.IsInStock(5))
103    fmt.Println("Total inventory value:", product.TotalValue())
104
105    // Use modifying methods
106    product.SetDescription("High-performance laptop with 16GB RAM")
107    fmt.Println("Updated description:", product.Description)
108
109    // Demonstrate safe stock management
110    err = product.ReduceStock(3)
111    if err != nil {
112        fmt.Println("Error reducing stock:", err)
113    } else {
114        fmt.Println("Stock reduced successfully")
115        fmt.Println("Current stock:", product.Stock)
116    }
117
118    fmt.Println(product) // Uses String() method
119}

Key insights from this example:

  • Constructor validation prevents invalid object creation
  • Value receivers for operations that don't modify state
  • Pointer receivers for operations that modify the object
  • Error handling in methods ensures data integrity
  • Method chaining creates fluent, readable APIs

Example 3: Composition in Action

Let's see how Go's composition creates flexible, maintainable code:

  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "time"
  7)
  8
  9// Base components
 10type Address struct {
 11    Street  string
 12    City    string
 13    State   string
 14    ZipCode string
 15    Country string
 16}
 17
 18type ContactInfo struct {
 19    Email string
 20    Phone string
 21}
 22
 23// Composed types
 24type Person struct {
 25    Name    string
 26    Age     int
 27    Address // Embedded Address
 28}
 29
 30type Customer struct {
 31    Person        // Embedded Person
 32    CustomerID   string
 33    Contact       ContactInfo // Nested ContactInfo
 34    JoinDate      time.Time
 35    OrdersValue   float64
 36}
 37
 38// Methods on base components
 39func (a Address) FullAddress() string {
 40    return fmt.Sprintf("%s, %s, %s %s, %s",
 41        a.Street, a.City, a.State, a.ZipCode, a.Country)
 42}
 43
 44func (c ContactInfo) GetPrimaryContact() string {
 45    if c.Email != "" {
 46        return c.Email
 47    }
 48    return c.Phone
 49}
 50
 51// Methods on composed types
 52func (c Customer) GetDisplayName() string {
 53    return c.Name // Accesses embedded Person.Name directly
 54}
 55
 56func (c Customer) CustomerSummary() string {
 57    return fmt.Sprintf("Customer %s joined on %v. Total orders: $%.2f",
 58        c.CustomerID, c.JoinDate.Format("2006-01-02"), c.OrdersValue)
 59}
 60
 61func (c *Customer) PlaceOrder(amount float64) error {
 62    if amount <= 0 {
 63        return fmt.Errorf("order amount must be positive")
 64    }
 65    c.OrdersValue += amount
 66    fmt.Printf("Order placed for %.2f. Total: %.2f\n", amount, c.OrdersValue)
 67    return nil
 68}
 69
 70func main() {
 71    // Create customer using composition
 72    customer := Customer{
 73        CustomerID: "CUST-001",
 74        Person: Person{
 75            Name: "Alice Johnson",
 76            Age:  32,
 77            Address: Address{
 78                Street:  "123 Main St",
 79                City:    "Boston",
 80                State:   "MA",
 81                ZipCode: "02101",
 82                Country: "USA",
 83            },
 84        },
 85        Contact: ContactInfo{
 86            Email: "alice@example.com",
 87            Phone: "(555) 123-4567",
 88        },
 89        JoinDate:    time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC),
 90        OrdersValue: 1500.00,
 91    }
 92
 93    // Use methods from all levels
 94    fmt.Println("Display name:", customer.GetDisplayName())
 95    fmt.Println("Full address:", customer.Address.FullAddress()) // Access embedded Address method
 96    fmt.Println("Primary contact:", customer.Contact.GetPrimaryContact())
 97    fmt.Println(customer.CustomerSummary())
 98
 99    // Demonstrate state modification
100    customer.PlaceOrder(99.99)
101}

What composition achieves:

  • Code reuse: Address and ContactInfo can be used in multiple contexts
  • Clear relationships: Customer HAS-A Person and ContactInfo
  • Method promotion: Customer can use Address methods directly
  • Flexible structure: Easy to add new fields or components
  • Type safety: All fields have compile-time type checking

Example 4: Advanced Patterns with Structs

Let's explore more sophisticated struct patterns:

  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "strings"
  8    "time"
  9)
 10
 11// Interface definition for polymorphism
 12type ReportGenerator interface {
 13    GenerateReport() string
 14}
 15
 16// Struct with methods for different report types
 17type SalesReport struct {
 18    Period     string
 19    Revenue    float64
 20    Orders     int
 21    Region     string
 22}
 23
 24type InventoryReport struct {
 25    Timestamp  time.Time
 26    TotalItems int
 27    LowStock   []string
 28}
 29
 30type ServiceReport struct {
 31    ServiceName string
 32    Uptime     float64
 33    Errors     int
 34}
 35
 36// Implement interface methods with value receivers
 37func (s SalesReport) GenerateReport() string {
 38    return fmt.Sprintf("Sales Report [%s]\nRevenue: $%.2f\nOrders: %d\nRegion: %s",
 39        s.Period, s.Revenue, s.Orders, s.Region)
 40}
 41
 42func (i InventoryReport) GenerateReport() string {
 43    return fmt.Sprintf("Inventory Report [%s]\nTotal Items: %d\nLow Stock Items: %v",
 44        i.Timestamp.Format("2006-01-02"), i.TotalItems, i.LowStock)
 45}
 46
 47func (s ServiceReport) GenerateReport() string {
 48    return fmt.Sprintf("Service Report: %s\nUptime: %.1f%%\nErrors: %d",
 49        s.ServiceName, s.Uptime, s.Errors)
 50}
 51
 52// Report manager that can handle any ReportGenerator
 53type ReportManager struct {
 54    reports []ReportGenerator
 55}
 56
 57func (rm *ReportManager) AddReport(report ReportGenerator) {
 58    rm.reports = append(rm.reports, report)
 59}
 60
 61func (rm *ReportManager) GenerateAllReports() string {
 62    var allReports strings.Builder
 63    for i, report := range rm.reports {
 64        allReports.WriteString(fmt.Sprintf("=== Report %d ===\n", i+1))
 65        allReports.WriteString(report.GenerateReport())
 66        allReports.WriteString("\n\n")
 67    }
 68    return allReports.String()
 69}
 70
 71// Struct with JSON tags for API serialization
 72type APIResponse struct {
 73    Success   bool        `json:"success"`
 74    Message   string      `json:"message"`
 75    Data      interface{} `json:"data,omitempty"`
 76    Timestamp time.Time   `json:"timestamp"`
 77    ErrorCode string      `json:"error_code,omitempty"`
 78}
 79
 80func (ar APIResponse) ToJSON() ([]byte, error) {
 81    return json.Marshal(ar)
 82}
 83
 84func main() {
 85    // Create different report types
 86    sales := SalesReport{
 87        Period:  "Q4 2023",
 88        Revenue: 150000.50,
 89        Orders: 1250,
 90        Region:  "North America",
 91    }
 92
 93    inventory := InventoryReport{
 94        Timestamp:  time.Now(),
 95        TotalItems: 5000,
 96        LowStock:   []string{"Laptop", "Mouse", "Keyboard"},
 97    }
 98
 99    service := ServiceReport{
100        ServiceName: "Payment Gateway",
101        Uptime:     99.9,
102        Errors:     3,
103    }
104
105    // Use polymorphic interface
106    manager := &ReportManager{}
107    manager.AddReport(sales)
108    manager.AddReport(inventory)
109    manager.AddReport(service)
110
111    fmt.Println(manager.GenerateAllReports())
112
113    // Demonstrate API response with JSON tags
114    response := APIResponse{
115        Success:   true,
116        Message:   "Reports generated successfully",
117        Data:      map[string]int{"reports": 3},
118        Timestamp: time.Now(),
119    }
120
121    jsonData, err := response.ToJSON()
122    if err != nil {
123        fmt.Println("Error generating JSON:", err)
124    } else {
125        fmt.Println("API Response:", string(jsonData))
126    }
127}

Advanced patterns demonstrated:

  • Interface satisfaction: Structs implement common interfaces
  • Polymorphism: Different types treated uniformly
  • Method sets: Value receivers for immutable data operations
  • Struct tags: Metadata for serialization formats
  • Generic handling: Manager works with any ReportGenerator

Common Patterns and Pitfalls

The Copy vs Reference Trap

Understanding when structs are copied vs referenced is crucial:

 1// ❌ WRONG: Assuming reference semantics
 2type Counter struct {
 3    Value int
 4}
 5
 6func modifyCounter(c Counter) {
 7    c.Value++ // Modifies a copy!
 8}
 9
10// ✅ CORRECT: Using pointer receiver for modification
11func (c *Counter) Increment() {
12    c.Value++ // Modifies original
13}

The Pointer Receiver Rules

Always use pointer receivers when:

  • The method modifies the receiver
  • The struct is large
  • Other methods use pointer receivers

Use value receivers when:

  • The method only reads data
  • The struct is small
  • You need immutability guarantees

Constructor Best Practices

 1// ✅ GOOD: Proper constructor with validation
 2func NewUser(id int, email string) (*User, error) {
 3    if id <= 0 {
 4        return nil, fmt.Errorf("invalid ID: %d", id)
 5    }
 6    if !strings.Contains(email, "@") {
 7        return nil, fmt.Errorf("invalid email: %s", email)
 8    }
 9
10    return &User{
11        ID:     id,
12        Email:  email,
13        // Other fields get zero values
14    }, nil
15}
16
17// ❌ AVOID: Incomplete initialization
18func BadUser(id int) *User {
19    return &User{ID: id} // Other fields are zero, might cause bugs
20}

Understanding Method Receivers in Depth

Method receivers are one of the most important concepts in Go's approach to object-oriented programming. Let's explore them in detail.

Value Receivers: Working with Copies

When you use a value receiver, the method operates on a copy of the struct:

 1// run
 2package main
 3
 4import "fmt"
 5
 6type Point struct {
 7    X, Y int
 8}
 9
10// Value receiver - works on a copy
11func (p Point) Scale(factor int) {
12    p.X *= factor
13    p.Y *= factor
14    fmt.Printf("Inside Scale: (%d, %d)\n", p.X, p.Y)
15}
16
17func main() {
18    p := Point{X: 3, Y: 4}
19    fmt.Printf("Before: (%d, %d)\n", p.X, p.Y)
20
21    p.Scale(2)
22
23    fmt.Printf("After:  (%d, %d)\n", p.X, p.Y) // Unchanged!
24}

Output:

Before: (3, 4)
Inside Scale: (6, 8)
After:  (3, 4)

Why use value receivers:

  • Immutability: Guarantees the original won't be modified
  • Thread-safety: Safe for concurrent reads
  • Small structs: Copying is cheap (< 64 bytes)
  • No nil checks: Value receivers can't be nil

Pointer Receivers: Working with Originals

When you use a pointer receiver, the method operates on the original struct:

 1// run
 2package main
 3
 4import "fmt"
 5
 6type Point struct {
 7    X, Y int
 8}
 9
10// Pointer receiver - works on original
11func (p *Point) Scale(factor int) {
12    p.X *= factor
13    p.Y *= factor
14    fmt.Printf("Inside Scale: (%d, %d)\n", p.X, p.Y)
15}
16
17func main() {
18    p := Point{X: 3, Y: 4}
19    fmt.Printf("Before: (%d, %d)\n", p.X, p.Y)
20
21    p.Scale(2) // Go automatically takes address
22
23    fmt.Printf("After:  (%d, %d)\n", p.X, p.Y) // Modified!
24}

Output:

Before: (3, 4)
Inside Scale: (6, 8)
After:  (6, 8)

Why use pointer receivers:

  • Modification: When you need to change the struct
  • Large structs: Avoid copying overhead
  • Consistency: If one method uses pointer, use for all
  • Interface satisfaction: Sometimes required for interfaces

Go's Automatic Dereferencing

Go automatically converts between pointers and values for method calls:

 1// run
 2package main
 3
 4import "fmt"
 5
 6type Counter struct {
 7    Value int
 8}
 9
10func (c *Counter) Increment() {
11    c.Value++
12}
13
14func (c Counter) GetValue() int {
15    return c.Value
16}
17
18func main() {
19    // Value variable
20    counter1 := Counter{Value: 0}
21    counter1.Increment()        // Go converts to &counter1
22    fmt.Println(counter1.GetValue())
23
24    // Pointer variable
25    counter2 := &Counter{Value: 0}
26    counter2.Increment()        // Already a pointer
27    fmt.Println(counter2.GetValue()) // Go converts to *counter2
28}

What Go does automatically:

  • counter1.Increment()(&counter1).Increment()
  • counter2.GetValue()(*counter2).GetValue()

Choosing the Right Receiver Type

Here's a decision tree for choosing receiver types:

 1// Decision: Does the method modify the receiver?
 2//   YES → Use pointer receiver
 3//   NO  → Continue to next question
 4
 5// Decision: Is the struct large (> 64 bytes)?
 6//   YES → Use pointer receiver (avoid copying)
 7//   NO  → Continue to next question
 8
 9// Decision: Do other methods use pointer receivers?
10//   YES → Use pointer receiver (consistency)
11//   NO  → Use value receiver
12
13// Example: Small, read-only struct
14type Color struct {
15    R, G, B byte
16}
17
18func (c Color) ToHex() string { // Value receiver
19    return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
20}
21
22// Example: Struct that needs modification
23type Account struct {
24    Balance float64
25}
26
27func (a *Account) Deposit(amount float64) { // Pointer receiver
28    a.Balance += amount
29}
30
31// Example: Large struct (even for reads)
32type LargeDataset struct {
33    Data [1000000]int
34}
35
36func (d *LargeDataset) Average() float64 { // Pointer receiver (avoid copying)
37    sum := 0
38    for _, v := range d.Data {
39        sum += v
40    }
41    return float64(sum) / float64(len(d.Data))
42}

Common Receiver Mistakes

Mistake 1: Mixing receiver types unnecessarily

 1// ❌ BAD: Inconsistent receivers
 2type Account struct {
 3    Balance float64
 4}
 5
 6func (a *Account) Deposit(amount float64) {
 7    a.Balance += amount
 8}
 9
10func (a Account) GetBalance() float64 { // Inconsistent!
11    return a.Balance
12}
13
14// ✅ GOOD: Consistent pointer receivers
15func (a *Account) Deposit(amount float64) {
16    a.Balance += amount
17}
18
19func (a *Account) GetBalance() float64 {
20    return a.Balance
21}

Mistake 2: Using value receivers for large structs

 1// ❌ BAD: Copying large struct on every call
 2type HugeStruct struct {
 3    Data [1000000]int
 4}
 5
 6func (h HugeStruct) Process() { // Copies 8MB!
 7    // ...
 8}
 9
10// ✅ GOOD: Using pointer to avoid copy
11func (h *HugeStruct) Process() {
12    // ...
13}

Mistake 3: Forgetting that value receivers can't modify

 1// ❌ BUG: Modifying copy doesn't affect original
 2type Config struct {
 3    Debug bool
 4}
 5
 6func (c Config) Enable() {
 7    c.Debug = true // Modifies copy
 8}
 9
10// ✅ CORRECT: Using pointer receiver
11func (c *Config) Enable() {
12    c.Debug = true // Modifies original
13}

Building Complete Systems

Let's create a complete, production-ready example that combines all concepts:

  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "os"
  8    "strings"
  9    "time"
 10)
 11
 12// Complete business domain model
 13type OrderStatus int
 14
 15const (
 16    OrderPending OrderStatus = iota
 17    OrderConfirmed
 18    OrderShipped
 19    OrderDelivered
 20    OrderCancelled
 21)
 22
 23type Order struct {
 24    ID          string    `json:"id"`
 25    CustomerID  string    `json:"customer_id"`
 26    ProductID   string    `json:"product_id"`
 27    Quantity    int       `json:"quantity"`
 28    Price       float64   `json:"price"`
 29    Status      OrderStatus `json:"status"`
 30    CreatedAt   time.Time  `json:"created_at"`
 31    UpdatedAt   time.Time  `json:"updated_at"`
 32}
 33
 34type Customer struct {
 35    ID        string    `json:"id"`
 36    Name      string    `json:"name"`
 37    Email     string    `json:"email"`
 38    Address   Address    `json:"address"`
 39    CreatedAt time.Time  `json:"created_at"`
 40}
 41
 42type Address struct {
 43    Street  string `json:"street"`
 44    City    string `json:"city"`
 45    State   string `json:"state"`
 46    ZipCode string `json:"zip_code"`
 47}
 48
 49type Product struct {
 50    ID          string  `json:"id"`
 51    Name        string  `json:"name"`
 52    Description string  `json:"description"`
 53    Price       float64 `json:"price"`
 54    Stock       int     `json:"stock"`
 55}
 56
 57// Order management system
 58type OrderSystem struct {
 59    customers map[string]*Customer
 60    products  map[string]*Product
 61    orders    []*Order
 62}
 63
 64func NewOrderSystem() *OrderSystem {
 65    return &OrderSystem{
 66        customers: map[string]*Customer{
 67            "cust-001": {
 68                ID:   "cust-001",
 69                Name: "Alice Johnson",
 70                Email: "alice@example.com",
 71                Address: Address{
 72                    Street:  "123 Main St",
 73                    City:   "Boston",
 74                    State:  "MA",
 75                    ZipCode: "02101",
 76                },
 77                CreatedAt: time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC),
 78            },
 79        },
 80        products: map[string]*Product{
 81            "prod-001": {
 82                ID:          "prod-001",
 83                Name:        "Laptop Pro",
 84                Description: "High-performance laptop",
 85                Price:       1299.99,
 86                Stock:       50,
 87            },
 88        },
 89        orders: make([]*Order, 0),
 90    }
 91}
 92
 93// Customer management methods
 94func (os *OrderSystem) AddCustomer(customer *Customer) error {
 95    if _, exists := os.customers[customer.ID]; exists {
 96        return fmt.Errorf("customer %s already exists", customer.ID)
 97    }
 98    os.customers[customer.ID] = customer
 99    return nil
100}
101
102func (os *OrderSystem) GetCustomer(id string) (*Customer, error) {
103    customer, exists := os.customers[id]
104    if !exists {
105        return nil, fmt.Errorf("customer %s not found", id)
106    }
107    return customer, nil
108}
109
110// Product management methods
111func (os *OrderSystem) UpdateStock(productID string, quantity int) error {
112    product, exists := os.products[productID]
113    if !exists {
114        return fmt.Errorf("product %s not found", productID)
115    }
116
117    product.Stock += quantity
118    return nil
119}
120
121func (os *OrderSystem) IsProductAvailable(productID string, quantity int) bool {
122    product, exists := os.products[productID]
123    if !exists {
124        return false
125    }
126    return product.Stock >= quantity
127}
128
129// Order processing methods
130func (os *OrderSystem) PlaceOrder(customerID, productID string, quantity int) (*Order, error) {
131    // Validate customer exists
132    _, err := os.GetCustomer(customerID)
133    if err != nil {
134        return nil, fmt.Errorf("invalid customer: %w", err)
135    }
136
137    // Validate product exists and has stock
138    if !os.IsProductAvailable(productID, quantity) {
139        return nil, fmt.Errorf("product %s not available in quantity %d", productID, quantity)
140    }
141
142    product := os.products[productID]
143
144    // Create order
145    order := &Order{
146        ID:        fmt.Sprintf("ORD-%d", len(os.orders)+1),
147        CustomerID: customerID,
148        ProductID: productID,
149        Quantity:  quantity,
150        Price:     product.Price,
151        Status:    OrderPending,
152        CreatedAt: time.Now(),
153        UpdatedAt: time.Now(),
154    }
155
156    // Reserve stock
157    os.UpdateStock(productID, -quantity)
158
159    os.orders = append(os.orders, order)
160    return order, nil
161}
162
163func (os *OrderSystem) UpdateOrderStatus(orderID string, status OrderStatus) error {
164    for _, order := range os.orders {
165        if order.ID == orderID {
166            order.Status = status
167            order.UpdatedAt = time.Now()
168            return nil
169        }
170    }
171    return fmt.Errorf("order %s not found", orderID)
172}
173
174// Reporting methods
175func (os *OrderSystem) GetOrderStatusText(status OrderStatus) string {
176    switch status {
177    case OrderPending:
178        return "Pending"
179    case OrderConfirmed:
180        return "Confirmed"
181    case OrderShipped:
182        return "Shipped"
183    case OrderDelivered:
184        return "Delivered"
185    case OrderCancelled:
186        return "Cancelled"
187    default:
188        return "Unknown"
189    }
190}
191
192func (os *OrderSystem) GenerateOrderReport() string {
193    var report strings.Builder
194    report.WriteString("=== Order Report ===\n")
195    report.WriteString(fmt.Sprintf("Total Orders: %d\n", len(os.orders)))
196
197    statusCount := make(map[OrderStatus]int)
198    for _, order := range os.orders {
199        statusCount[order.Status]++
200    }
201
202    for status, count := range statusCount {
203        report.WriteString(fmt.Sprintf("%s: %d\n", os.GetOrderStatusText(status), count))
204    }
205
206    return report.String()
207}
208
209// Export functionality
210func (os *OrderSystem) ExportOrders(filename string) error {
211    data, err := json.MarshalIndent(os.orders, "", "  ")
212    if err != nil {
213        return fmt.Errorf("failed to marshal orders: %w", err)
214    }
215
216    err = os.WriteFile(filename, data, 0644)
217    if err != nil {
218        return fmt.Errorf("failed to write orders to file: %w", err)
219    }
220
221    fmt.Printf("Orders exported to %s\n", filename)
222    return nil
223}
224
225// Method for string representation
226func (os *OrderSystem) String() string {
227    return fmt.Sprintf("OrderSystem{customers:%d, products:%d, orders:%d}",
228        len(os.customers), len(os.products), len(os.orders))
229}
230
231func main() {
232    // Initialize order system
233    system := NewOrderSystem()
234    fmt.Println("Order System initialized:")
235    fmt.Println(system)
236
237    // Demonstrate order processing
238    fmt.Println("\n=== Placing Orders ===")
239
240    order1, err := system.PlaceOrder("cust-001", "prod-001", 2)
241    if err != nil {
242        fmt.Println("Error placing order 1:", err)
243    } else {
244        fmt.Printf("Order placed: %s\n", order1.ID)
245    }
246
247    order2, err := system.PlaceOrder("cust-001", "prod-001", 1)
248    if err != nil {
249        fmt.Println("Error placing order 2:", err)
250    } else {
251        fmt.Printf("Order placed: %s\n", order2.ID)
252    }
253
254    // Update order statuses
255    fmt.Println("\n=== Updating Order Status ===")
256    system.UpdateOrderStatus(order1.ID, OrderConfirmed)
257    system.UpdateOrderStatus(order2.ID, OrderShipped)
258
259    // Generate and display reports
260    fmt.Println("\n=== Order Report ===")
261    fmt.Println(system.GenerateOrderReport())
262
263    // Export data
264    fmt.Println("\n=== Exporting Orders ===")
265    err = system.ExportOrders("orders.json")
266    if err != nil {
267        fmt.Println("Error exporting orders:", err)
268    }
269}

This comprehensive example demonstrates:

  • Domain modeling: Complete business entities with relationships
  • Constructor pattern: Proper initialization and setup
  • Method organization: Logical grouping of operations
  • Error handling: Graceful failure management
  • Data validation: Input checking and error reporting
  • State management: Proper use of pointer receivers
  • Serialization: JSON tags for data interchange
  • Reporting: High-level summary generation
  • File operations: External data export

Struct Tags and Metadata

Struct tags provide metadata about fields, commonly used for serialization, validation, and database mapping:

 1// run
 2package main
 3
 4import (
 5    "encoding/json"
 6    "fmt"
 7)
 8
 9type User struct {
10    // JSON serialization tags
11    ID       int    `json:"id"`
12    Username string `json:"username"`
13    Email    string `json:"email,omitempty"`    // Omit if empty
14    Password string `json:"-"`                   // Never serialize
15
16    // Multiple tags for different purposes
17    Age      int    `json:"age" validate:"min=0,max=150"`
18    Active   bool   `json:"is_active" db:"active"`
19}
20
21func main() {
22    user := User{
23        ID:       1,
24        Username: "alice",
25        Email:    "alice@example.com",
26        Password: "secret123",
27        Age:      25,
28        Active:   true,
29    }
30
31    // Serialize to JSON
32    data, err := json.MarshalIndent(user, "", "  ")
33    if err != nil {
34        fmt.Println("Error:", err)
35        return
36    }
37
38    fmt.Println("JSON Output:")
39    fmt.Println(string(data))
40    fmt.Println("\nNote: Password field is not included in JSON")
41
42    // Deserialize from JSON
43    jsonStr := `{"id":2,"username":"bob","age":30,"is_active":true}`
44    var user2 User
45    err = json.Unmarshal([]byte(jsonStr), &user2)
46    if err != nil {
47        fmt.Println("Error:", err)
48        return
49    }
50
51    fmt.Println("\nDeserialized User:")
52    fmt.Printf("%+v\n", user2)
53}

Common struct tag formats:

  • json:"field_name" - JSON field name
  • json:"field_name,omitempty" - Omit if zero value
  • json:"-" - Never serialize
  • db:"column_name" - Database column name
  • validate:"required,min=1" - Validation rules

Advanced Composition Patterns

Embedding Multiple Types

 1// run
 2package main
 3
 4import "fmt"
 5
 6type Identifiable struct {
 7    ID string
 8}
 9
10type Timestamped struct {
11    CreatedAt string
12    UpdatedAt string
13}
14
15type Versioned struct {
16    Version int
17}
18
19// Entity embeds multiple types
20type Entity struct {
21    Identifiable
22    Timestamped
23    Versioned
24    Name string
25}
26
27func main() {
28    entity := Entity{
29        Identifiable: Identifiable{ID: "123"},
30        Timestamped:  Timestamped{CreatedAt: "2023-01-01", UpdatedAt: "2023-06-01"},
31        Versioned:    Versioned{Version: 1},
32        Name:         "My Entity",
33    }
34
35    // Access embedded fields directly
36    fmt.Println("ID:", entity.ID)
37    fmt.Println("Created:", entity.CreatedAt)
38    fmt.Println("Version:", entity.Version)
39    fmt.Println("Name:", entity.Name)
40}

Field Name Conflicts

When embedded types have fields with the same name, you must qualify them:

 1// run
 2package main
 3
 4import "fmt"
 5
 6type BaseA struct {
 7    Name string
 8}
 9
10type BaseB struct {
11    Name string
12}
13
14type Combined struct {
15    BaseA
16    BaseB
17    OwnName string
18}
19
20func main() {
21    c := Combined{
22        BaseA:   BaseA{Name: "Name from A"},
23        BaseB:   BaseB{Name: "Name from B"},
24        OwnName: "Combined's own name",
25    }
26
27    // Must qualify conflicting names
28    fmt.Println("BaseA.Name:", c.BaseA.Name)
29    fmt.Println("BaseB.Name:", c.BaseB.Name)
30    fmt.Println("OwnName:", c.OwnName)
31
32    // fmt.Println(c.Name) // Error: ambiguous selector
33}

Memory Layout and Performance

Understanding struct memory layout helps write efficient code:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "unsafe"
 7)
 8
 9// Poor layout - lots of padding
10type BadLayout struct {
11    a byte   // 1 byte + 7 padding
12    b int64  // 8 bytes
13    c byte   // 1 byte + 7 padding
14    d int64  // 8 bytes
15}
16
17// Good layout - minimal padding
18type GoodLayout struct {
19    b int64  // 8 bytes
20    d int64  // 8 bytes
21    a byte   // 1 byte
22    c byte   // 1 byte + 6 padding
23}
24
25func main() {
26    fmt.Println("BadLayout size:", unsafe.Sizeof(BadLayout{}))   // 32 bytes
27    fmt.Println("GoodLayout size:", unsafe.Sizeof(GoodLayout{})) // 24 bytes
28
29    fmt.Println("\nTip: Order struct fields from largest to smallest")
30    fmt.Println("     to minimize padding and reduce memory usage")
31}

Memory optimization tips:

  • Order fields from largest to smallest
  • Group related fields together
  • Use smaller types when possible (byte instead of int)
  • Be aware of padding for alignment

Practice Exercises

Exercise 1: Person Struct with Methods

Learning Objective: Master basic struct creation, field access, and method implementation with both value and pointer receivers.

Real-World Context: Structs with methods are the foundation of object-oriented programming in Go. They're used everywhere from user profiles and configuration objects to complex business entities. Understanding the difference between value and pointer receivers is crucial for correct data manipulation.

Difficulty: Beginner
Time Estimate: 20 minutes

Create a Person struct with fields for name, age, and email. Add methods to print a greeting and check if the person is an adult. Practice implementing both value and pointer receiver methods to understand when each is appropriate.

Solution
 1// run
 2package main
 3
 4import "fmt"
 5
 6type Person struct {
 7    Name  string
 8    Age   int
 9    Email string
10}
11
12func NewPerson(name string, age int, email string) *Person {
13    return &Person{
14        Name:  name,
15        Age:   age,
16        Email: email,
17    }
18}
19
20func (p Person) Greet() string {
21    return fmt.Sprintf("Hello, my name is %s and I'm %d years old.", p.Name, p.Age)
22}
23
24func (p Person) IsAdult() bool {
25    return p.Age >= 18
26}
27
28func (p *Person) HaveBirthday() {
29    p.Age++
30    fmt.Printf("Happy Birthday %s! You're now %d years old.\n", p.Name, p.Age)
31}
32
33func main() {
34    person := NewPerson("Alice", 17, "alice@example.com")
35
36    fmt.Println(person.Greet())
37    fmt.Println("Is adult?", person.IsAdult())
38
39    person.HaveBirthday()
40    fmt.Println("Is adult now?", person.IsAdult())
41}

Exercise 2: Rectangle with Area and Perimeter

Learning Objective: Practice geometric calculations with methods and understand when to use value vs pointer receivers.

Real-World Context: Geometric shapes are commonly used in graphics programming, game development, and CAD systems. This exercise teaches how to implement both read-only operations and mutating operations with appropriate receiver types.

Difficulty: Beginner
Time Estimate: 25 minutes

Create a Rectangle struct with width and height, then add methods to calculate area, perimeter, and check if it's a square. Implement both value receiver methods and pointer receiver methods.

Solution
 1// run
 2package main
 3
 4import "fmt"
 5
 6type Rectangle struct {
 7    Width  float64
 8    Height float64
 9}
10
11func NewRectangle(width, height float64) *Rectangle {
12    if width <= 0 || height <= 0 {
13        return &Rectangle{Width: 1, Height: 1} // Default to 1x1 for invalid input
14    }
15    return &Rectangle{Width: width, Height: height}
16}
17
18func (r Rectangle) Area() float64 {
19    return r.Width * r.Height
20}
21
22func (r Rectangle) Perimeter() float64 {
23    return 2 * (r.Width + r.Height)
24}
25
26func (r Rectangle) IsSquare() bool {
27    return r.Width == r.Height
28}
29
30func (r *Rectangle) Scale(factor float64) {
31    r.Width *= factor
32    r.Height *= factor
33}
34
35func (r *Rectangle) SetDimensions(width, height float64) error {
36    if width <= 0 || height <= 0 {
37        return fmt.Errorf("dimensions must be positive")
38    }
39    r.Width = width
40    r.Height = height
41    return nil
42}
43
44func (r Rectangle) String() string {
45    return fmt.Sprintf("Rectangle[Width: %.2f, Height: %.2f]", r.Width, r.Height)
46}
47
48func main() {
49    rect := NewRectangle(10, 5)
50
51    fmt.Println(rect)
52    fmt.Printf("Area: %.2f\n", rect.Area())
53    fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
54    fmt.Printf("Is square? %v\n", rect.IsSquare())
55
56    rect.Scale(2)
57    fmt.Println("\nAfter scaling by 2:")
58    fmt.Println(rect)
59    fmt.Printf("Area: %.2f\n", rect.Area())
60
61    err := rect.SetDimensions(5, 5)
62    if err != nil {
63        fmt.Println("Error setting dimensions:", err)
64    } else {
65        fmt.Println("\nSet to 5x5:")
66        fmt.Println(rect)
67        fmt.Printf("Is square? %v\n", rect.IsSquare())
68    }
69}

Exercise 3: Bank Account with Transaction History

Learning Objective: Master complex struct design with nested structs, pointer receivers for state modification, and transaction logging.

Real-World Context: Banking applications require careful state management and audit trails. This exercise demonstrates how to design systems that maintain historical records, validate operations, and provide detailed reporting - patterns used in financial software, inventory management, and audit systems.

Difficulty: Intermediate
Time Estimate: 30 minutes

Create a BankAccount struct that tracks deposits, withdrawals, and maintains a transaction history. Practice using nested structs, pointer receivers for state changes, and implementing business logic with validation and error handling.

Solution
  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "time"
  7)
  8
  9type Transaction struct {
 10    Type      string
 11    Amount    float64
 12    Timestamp time.Time
 13    Balance   float64
 14}
 15
 16type BankAccount struct {
 17    AccountNumber string
 18    HolderName    string
 19    Balance       float64
 20    Transactions  []Transaction
 21}
 22
 23func NewBankAccount(accountNumber, holderName string, initialBalance float64) *BankAccount {
 24    if initialBalance < 0 {
 25        initialBalance = 0
 26    }
 27
 28    return &BankAccount{
 29        AccountNumber: accountNumber,
 30        HolderName:    holderName,
 31        Balance:       initialBalance,
 32        Transactions: []Transaction{
 33            {
 34                Type:      "deposit",
 35                Amount:    initialBalance,
 36                Timestamp: time.Now(),
 37                Balance:   initialBalance,
 38            },
 39        },
 40    }
 41}
 42
 43func (ba *BankAccount) Deposit(amount float64) error {
 44    if amount <= 0 {
 45        return fmt.Errorf("deposit amount must be positive")
 46    }
 47
 48    ba.Balance += amount
 49    ba.Transactions = append(ba.Transactions, Transaction{
 50        Type:      "deposit",
 51        Amount:    amount,
 52        Timestamp: time.Now(),
 53        Balance:   ba.Balance,
 54    })
 55
 56    fmt.Printf("Deposited $%.2f. New balance: $%.2f\n", amount, ba.Balance)
 57    return nil
 58}
 59
 60func (ba *BankAccount) Withdraw(amount float64) error {
 61    if amount <= 0 {
 62        return fmt.Errorf("withdrawal amount must be positive")
 63    }
 64
 65    if amount > ba.Balance {
 66        return fmt.Errorf("insufficient funds")
 67    }
 68
 69    ba.Balance -= amount
 70    ba.Transactions = append(ba.Transactions, Transaction{
 71        Type:      "withdrawal",
 72        Amount:    amount,
 73        Timestamp: time.Now(),
 74        Balance:   ba.Balance,
 75    })
 76
 77    fmt.Printf("Withdrew $%.2f. New balance: $%.2f\n", amount, ba.Balance)
 78    return nil
 79}
 80
 81func (ba BankAccount) GetBalance() float64 {
 82    return ba.Balance
 83}
 84
 85func (ba BankAccount) PrintStatement() {
 86    fmt.Printf("\n=== Account Statement for %s ===\n", ba.HolderName)
 87    fmt.Printf("Account: %s\n", ba.AccountNumber)
 88    fmt.Printf("Current Balance: $%.2f\n\n", ba.Balance)
 89    fmt.Println("Transaction History:")
 90
 91    for _, tx := range ba.Transactions {
 92        fmt.Printf("%s | %-12s | $%8.2f | Balance: $%8.2f\n",
 93            tx.Timestamp.Format("2006-01-02 15:04:05"),
 94            tx.Type,
 95            tx.Amount,
 96            tx.Balance)
 97    }
 98}
 99
100func main() {
101    account := NewBankAccount("123456", "Alice Johnson", 1000.00)
102
103    account.Deposit(500.00)
104    account.Withdraw(200.00)
105    account.Deposit(150.00)
106
107    err := account.Withdraw(2000.00)
108    if err != nil {
109        fmt.Println("Error:", err)
110    }
111
112    account.PrintStatement()
113}

Exercise 4: Student Gradebook

Learning Objective: Master complex data modeling with nested structs, slice management, and business logic implementation.

Real-World Context: Educational management systems require complex calculations and data relationships. This exercise demonstrates how to implement GPA calculations, academic standing determination, and transcript generation - patterns used in student information systems, HR performance tracking, and analytics platforms.

Difficulty: Intermediate
Time Estimate: 35 minutes

Create a Student struct with a gradebook that can calculate GPA, add courses, and determine academic standing. Practice working with slices of structs, implementing complex business calculations, and creating comprehensive reporting methods.

Solution
  1// run
  2package main
  3
  4import "fmt"
  5
  6type Course struct {
  7    Name   string
  8    Grade  float64
  9    Credits int
 10}
 11
 12type Student struct {
 13    ID      string
 14    Name    string
 15    Courses []Course
 16}
 17
 18func NewStudent(id, name string) *Student {
 19    return &Student{
 20        ID:      id,
 21        Name:    name,
 22        Courses: make([]Course, 0),
 23    }
 24}
 25
 26func (s *Student) AddCourse(name string, grade float64, credits int) error {
 27    if grade < 0 || grade > 100 {
 28        return fmt.Errorf("grade must be between 0 and 100")
 29    }
 30    if credits <= 0 {
 31        return fmt.Errorf("credits must be positive")
 32    }
 33
 34    course := Course{
 35        Name:   name,
 36        Grade:  grade,
 37        Credits: credits,
 38    }
 39    s.Courses = append(s.Courses, course)
 40    fmt.Printf("Added course: %s (Grade: %.1f, Credits: %d)\n", name, grade, credits)
 41    return nil
 42}
 43
 44func (s Student) CalculateGPA() float64 {
 45    if len(s.Courses) == 0 {
 46        return 0.0
 47    }
 48
 49    var totalPoints float64
 50    var totalCredits int
 51
 52    for _, course := range s.Courses {
 53        gradePoint := s.gradeToGPA(course.Grade)
 54        totalPoints += gradePoint * float64(course.Credits)
 55        totalCredits += course.Credits
 56    }
 57
 58    if totalCredits == 0 {
 59        return 0.0
 60    }
 61
 62    return totalPoints / float64(totalCredits)
 63}
 64
 65func (s Student) gradeToGPA(grade float64) float64 {
 66    switch {
 67    case grade >= 90:
 68        return 4.0
 69    case grade >= 80:
 70        return 3.0
 71    case grade >= 70:
 72        return 2.0
 73    case grade >= 60:
 74        return 1.0
 75    default:
 76        return 0.0
 77    }
 78}
 79
 80func (s Student) GetAcademicStanding() string {
 81    gpa := s.CalculateGPA()
 82
 83    switch {
 84    case gpa >= 3.5:
 85        return "Dean's List"
 86    case gpa >= 3.0:
 87        return "Good Standing"
 88    case gpa >= 2.0:
 89        return "Satisfactory"
 90    case gpa >= 1.0:
 91        return "Academic Warning"
 92    default:
 93        return "Academic Probation"
 94    }
 95}
 96
 97func (s Student) PrintTranscript() {
 98    fmt.Printf("\n=== Transcript for %s (%s) ===\n", s.Name, s.ID)
 99
100    for _, course := range s.Courses {
101        gradePoint := s.gradeToGPA(course.Grade)
102        fmt.Printf("%-30s | Grade: %5.1f | Credits: %d | GPA: %.1f\n",
103            course.Name, course.Grade, course.Credits, gradePoint)
104    }
105
106    fmt.Printf("\nOverall GPA: %.2f\n", s.CalculateGPA())
107    fmt.Printf("Academic Standing: %s\n", s.GetAcademicStanding())
108}
109
110func main() {
111    student := NewStudent("S12345", "Bob Smith")
112
113    student.AddCourse("Math 101", 92, 4)
114    student.AddCourse("English 101", 85, 3)
115    student.AddCourse("Computer Science 101", 95, 4)
116    student.AddCourse("History 101", 78, 3)
117
118    student.PrintTranscript()
119}

Exercise 5: Todo List Manager with Filtering

Learning Objective: Master advanced struct composition, custom types, enum-like constants, and complex data manipulation algorithms.

Real-World Context: Task management systems are common in productivity applications and project management software. This exercise demonstrates filtering, sorting, and state management patterns used in applications like Jira, Asana, and personal productivity tools.

Difficulty: Advanced
Time Estimate: 40 minutes

Create a comprehensive todo list manager with task priorities, deadlines, filtering, and sorting capabilities. Practice implementing custom types with methods, complex filtering logic, and multiple sorting algorithms.

Solution
  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "sort"
  7    "time"
  8)
  9
 10type Priority int
 11
 12const (
 13    Low Priority = iota
 14    Medium
 15    High
 16    Urgent
 17)
 18
 19func (p Priority) String() string {
 20    return [...]string{"Low", "Medium", "High", "Urgent"}[p]
 21}
 22
 23type Task struct {
 24    ID          int
 25    Title       string
 26    Description string
 27    Priority    Priority
 28    Completed   bool
 29    CreatedAt   time.Time
 30    DueDate     *time.Time
 31}
 32
 33type TodoList struct {
 34    Tasks  []Task
 35    nextID int
 36}
 37
 38func NewTodoList() *TodoList {
 39    return &TodoList{
 40        Tasks:  make([]Task, 0),
 41        nextID: 1,
 42    }
 43}
 44
 45func (t *TodoList) AddTask(title, description string, priority Priority, dueDate *time.Time) *Task {
 46    task := Task{
 47        ID:          t.nextID,
 48        Title:       title,
 49        Description: description,
 50        Priority:    priority,
 51        Completed:   false,
 52        CreatedAt:   time.Now(),
 53        DueDate:     dueDate,
 54    }
 55
 56    t.Tasks = append(t.Tasks, task)
 57    t.nextID++
 58
 59    fmt.Printf("Added task #%d: %s [%s]\n", task.ID, task.Title, task.Priority)
 60    return &task
 61}
 62
 63func (t *TodoList) CompleteTask(id int) error {
 64    for i := range t.Tasks {
 65        if t.Tasks[i].ID == id {
 66            t.Tasks[i].Completed = true
 67            fmt.Printf("Completed task #%d: %s\n", id, t.Tasks[i].Title)
 68            return nil
 69        }
 70    }
 71    return fmt.Errorf("task #%d not found", id)
 72}
 73
 74func (t TodoList) GetPendingTasks() []Task {
 75    pending := make([]Task, 0)
 76    for _, task := range t.Tasks {
 77        if !task.Completed {
 78            pending = append(pending, task)
 79        }
 80    }
 81    return pending
 82}
 83
 84func (t TodoList) GetTasksByPriority(priority Priority) []Task {
 85    filtered := make([]Task, 0)
 86    for _, task := range t.Tasks {
 87        if task.Priority == priority && !task.Completed {
 88            filtered = append(filtered, task)
 89        }
 90    }
 91    return filtered
 92}
 93
 94func (t TodoList) GetOverdueTasks() []Task {
 95    overdue := make([]Task, 0)
 96    now := time.Now()
 97
 98    for _, task := range t.Tasks {
 99        if !task.Completed && task.DueDate != nil && now.After(*task.DueDate) {
100            overdue = append(overdue, task)
101        }
102    }
103    return overdue
104}
105
106// Sort by Priority
107func (t *TodoList) SortByPriority() {
108    sort.Slice(t.Tasks, func(i, j int) bool {
109        if t.Tasks[i].Completed != t.Tasks[j].Completed {
110            return !t.Tasks[i].Completed
111        }
112        if !t.Tasks[i].Completed && !t.Tasks[j].Completed {
113            return t.Tasks[i].Priority > t.Tasks[j].Priority
114        }
115        return true
116    })
117}
118
119// Sort by Due Date
120func (t *TodoList) SortByDueDate() {
121    sort.Slice(t.Tasks, func(i, j int) bool {
122        if t.Tasks[i].Completed != t.Tasks[j].Completed {
123            return !t.Tasks[i].Completed
124        }
125        if !t.Tasks[i].Completed && !t.Tasks[j].Completed {
126            if t.Tasks[i].DueDate == nil {
127                return false
128            }
129            if t.Tasks[j].DueDate == nil {
130                return true
131            }
132            return t.Tasks[i].DueDate.Before(*t.Tasks[j].DueDate)
133        }
134        return true
135    })
136}
137
138func (t TodoList) PrintTasks(tasks []Task) {
139    if len(tasks) == 0 {
140        fmt.Println("No tasks to display.")
141        return
142    }
143
144    for _, task := range tasks {
145        status := "[ ]"
146        if task.Completed {
147            status = "[✓]"
148        }
149
150        dueDateStr := "No deadline"
151        if task.DueDate != nil {
152            dueDateStr = task.DueDate.Format("2006-01-02")
153        }
154
155        fmt.Printf("%s #%d: %s [%s] - Due: %s\n",
156            status, task.ID, task.Title, task.Priority, dueDateStr)
157        if task.Description != "" {
158            fmt.Printf("    %s\n", task.Description)
159        }
160    }
161}
162
163func main() {
164    todo := NewTodoList()
165
166    // Add tasks with different priorities and due dates
167    tomorrow := time.Now().Add(24 * time.Hour)
168    nextWeek := time.Now().Add(7 * 24 * time.Hour)
169    yesterday := time.Now().Add(-24 * time.Hour)
170
171    todo.AddTask("Fix critical bug", "Production server down", Urgent, &tomorrow)
172    todo.AddTask("Write documentation", "API docs needed", Medium, &nextWeek)
173    todo.AddTask("Code review", "Review PR #123", High, &yesterday)
174    todo.AddTask("Update dependencies", "Security patches", Low, nil)
175
176    fmt.Println("\n=== All Tasks ===")
177    todo.SortByPriority()
178    todo.PrintTasks(todo.Tasks)
179
180    fmt.Println("\n=== Pending Tasks ===")
181    todo.PrintTasks(todo.GetPendingTasks())
182
183    fmt.Println("\n=== High Priority Tasks ===")
184    todo.PrintTasks(todo.GetTasksByPriority(High))
185
186    fmt.Println("\n=== Overdue Tasks ===")
187    todo.PrintTasks(todo.GetOverdueTasks())
188
189    // Complete a task
190    fmt.Println()
191    todo.CompleteTask(1)
192
193    fmt.Println("\n=== Pending Tasks After Completion ===")
194    todo.PrintTasks(todo.GetPendingTasks())
195}

Exercise 6: Library Management System

Learning Objective: Master complex system design with multiple interrelated structs, business logic implementation, and comprehensive state management.

Real-World Context: Library management systems demonstrate enterprise-level software design patterns including entity relationships, inventory management, transaction logging, and search functionality. These patterns apply to e-commerce systems, inventory management, and resource tracking applications.

Difficulty: Advanced
Time Estimate: 45 minutes

Create a library management system that tracks books, members, and checkouts. Implement borrowing logic, due date tracking, fine calculation, and search functionality. This exercise demonstrates how to build systems with multiple entities that interact through well-defined interfaces.

Solution
  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "strings"
  7    "time"
  8)
  9
 10type Book struct {
 11    ISBN      string
 12    Title     string
 13    Author    string
 14    Available bool
 15}
 16
 17type Member struct {
 18    ID        string
 19    Name      string
 20    Email     string
 21    BooksOut  []string // ISBNs of books currently checked out
 22}
 23
 24type Checkout struct {
 25    ISBN       string
 26    MemberID   string
 27    CheckoutDate time.Time
 28    DueDate      time.Time
 29    ReturnDate   *time.Time
 30}
 31
 32type Library struct {
 33    Books     map[string]*Book
 34    Members   map[string]*Member
 35    Checkouts []Checkout
 36}
 37
 38func NewLibrary() *Library {
 39    return &Library{
 40        Books:     make(map[string]*Book),
 41        Members:   make(map[string]*Member),
 42        Checkouts: make([]Checkout, 0),
 43    }
 44}
 45
 46func (l *Library) AddBook(isbn, title, author string) error {
 47    if _, exists := l.Books[isbn]; exists {
 48        return fmt.Errorf("book with ISBN %s already exists", isbn)
 49    }
 50
 51    l.Books[isbn] = &Book{
 52        ISBN:      isbn,
 53        Title:     title,
 54        Author:    author,
 55        Available: true,
 56    }
 57
 58    fmt.Printf("Added book: %s by %s\n", title, author)
 59    return nil
 60}
 61
 62func (l *Library) AddMember(id, name, email string) error {
 63    if _, exists := l.Members[id]; exists {
 64        return fmt.Errorf("member with ID %s already exists", id)
 65    }
 66
 67    l.Members[id] = &Member{
 68        ID:       id,
 69        Name:     name,
 70        Email:    email,
 71        BooksOut: make([]string, 0),
 72    }
 73
 74    fmt.Printf("Added member: %s\n", name)
 75    return nil
 76}
 77
 78func (l *Library) CheckoutBook(isbn, memberID string) error {
 79    book, exists := l.Books[isbn]
 80    if !exists {
 81        return fmt.Errorf("book with ISBN %s not found", isbn)
 82    }
 83
 84    if !book.Available {
 85        return fmt.Errorf("book is not available")
 86    }
 87
 88    member, exists := l.Members[memberID]
 89    if !exists {
 90        return fmt.Errorf("member with ID %s not found", memberID)
 91    }
 92
 93    // Limit books per member
 94    if len(member.BooksOut) >= 3 {
 95        return fmt.Errorf("member has reached checkout limit")
 96    }
 97
 98    // Create checkout record
 99    checkout := Checkout{
100        ISBN:         isbn,
101        MemberID:     memberID,
102        CheckoutDate: time.Now(),
103        DueDate:      time.Now().Add(14 * 24 * time.Hour), // 2 weeks
104        ReturnDate:   nil,
105    }
106
107    l.Checkouts = append(l.Checkouts, checkout)
108    book.Available = false
109    member.BooksOut = append(member.BooksOut, isbn)
110
111    fmt.Printf("Checked out '%s' to %s (Due: %s)\n",
112        book.Title, member.Name, checkout.DueDate.Format("2006-01-02"))
113
114    return nil
115}
116
117func (l *Library) ReturnBook(isbn, memberID string) error {
118    book, exists := l.Books[isbn]
119    if !exists {
120        return fmt.Errorf("book with ISBN %s not found", isbn)
121    }
122
123    member, exists := l.Members[memberID]
124    if !exists {
125        return fmt.Errorf("member with ID %s not found", memberID)
126    }
127
128    // Find active checkout
129    checkoutIndex := -1
130    for i, checkout := range l.Checkouts {
131        if checkout.ISBN == isbn && checkout.MemberID == memberID && checkout.ReturnDate == nil {
132            checkoutIndex = i
133            break
134        }
135    }
136
137    if checkoutIndex == -1 {
138        return fmt.Errorf("no active checkout found for this book and member")
139    }
140
141    // Process return
142    now := time.Now()
143    l.Checkouts[checkoutIndex].ReturnDate = &now
144
145    book.Available = true
146
147    // Remove from member's books out
148    for i, bookISBN := range member.BooksOut {
149        if bookISBN == isbn {
150            member.BooksOut = append(member.BooksOut[:i], member.BooksOut[i+1:]...)
151            break
152        }
153    }
154
155    // Calculate fine if overdue
156    checkout := l.Checkouts[checkoutIndex]
157    if now.After(checkout.DueDate) {
158        daysLate := int(now.Sub(checkout.DueDate).Hours() / 24)
159        fine := float64(daysLate) * 0.50
160        fmt.Printf("Book returned late! Fine: $%.2f (%d days overdue)\n", fine, daysLate)
161    } else {
162        fmt.Printf("Book '%s' returned on time\n", book.Title)
163    }
164
165    return nil
166}
167
168func (l Library) SearchBooks(query string) []Book {
169    query = strings.ToLower(query)
170    results := make([]Book, 0)
171
172    for _, book := range l.Books {
173        if strings.Contains(strings.ToLower(book.Title), query) ||
174            strings.Contains(strings.ToLower(book.Author), query) {
175            results = append(results, *book)
176        }
177    }
178
179    return results
180}
181
182func (l Library) GetOverdueCheckouts() []Checkout {
183    overdue := make([]Checkout, 0)
184    now := time.Now()
185
186    for _, checkout := range l.Checkouts {
187        if checkout.ReturnDate == nil && now.After(checkout.DueDate) {
188            overdue = append(overdue, checkout)
189        }
190    }
191
192    return overdue
193}
194
195func (l Library) PrintLibraryStatus() {
196    fmt.Println("\n=== Library Status ===")
197    fmt.Printf("Total Books: %d\n", len(l.Books))
198    fmt.Printf("Total Members: %d\n", len(l.Members))
199
200    availableCount := 0
201    for _, book := range l.Books {
202        if book.Available {
203            availableCount++
204        }
205    }
206    fmt.Printf("Available Books: %d\n", availableCount)
207    fmt.Printf("Checked Out Books: %d\n", len(l.Books)-availableCount)
208
209    overdueCheckouts := l.GetOverdueCheckouts()
210    fmt.Printf("Overdue Checkouts: %d\n", len(overdueCheckouts))
211}
212
213func main() {
214    library := NewLibrary()
215
216    // Add books
217    library.AddBook("978-0-13-110362-7", "The C Programming Language", "Kernighan & Ritchie")
218    library.AddBook("978-0-201-61622-4", "The Pragmatic Programmer", "Hunt & Thomas")
219    library.AddBook("978-0-13-468599-1", "The Go Programming Language", "Donovan & Kernighan")
220
221    // Add members
222    library.AddMember("M001", "Alice Johnson", "alice@example.com")
223    library.AddMember("M002", "Bob Smith", "bob@example.com")
224
225    // Checkout books
226    fmt.Println("\n=== Checkouts ===")
227    library.CheckoutBook("978-0-13-110362-7", "M001")
228    library.CheckoutBook("978-0-13-468599-1", "M002")
229
230    // Search books
231    fmt.Println("\n=== Search Results for 'programming' ===")
232    results := library.SearchBooks("programming")
233    for _, book := range results {
234        availStatus := "Available"
235        if !book.Available {
236            availStatus = "Checked Out"
237        }
238        fmt.Printf("- %s by %s [%s]\n", book.Title, book.Author, availStatus)
239    }
240
241    // Return books
242    fmt.Println("\n=== Returns ===")
243    library.ReturnBook("978-0-13-110362-7", "M001")
244
245    // Print status
246    library.PrintLibraryStatus()
247}

Summary

Key Takeaways

Structs provide:

  • Type-safe data organization with compile-time field checking
  • Efficient memory usage with contiguous field layout
  • Zero value safety with meaningful default values
  • Composition flexibility without inheritance complexity

Methods enable:

  • Data-behavior association that makes code intuitive
  • Interface satisfaction for polymorphism without inheritance
  • Explicit receiver types for clear modification vs read-only semantics
  • Method chaining for fluent, readable APIs

Best practices:

  • Use constructor functions for proper initialization
  • Pointer receivers for modification, value receivers for read-only
  • Composition over inheritance for flexible, maintainable code
  • Validation in constructors and methods for robust error handling
  • Struct tags for serialization and metadata management

Next Steps

Continue your Go learning journey with these essential topics:

  1. Interfaces - Learn how Go achieves polymorphism with interfaces, enabling flexible APIs and dependency injection
  2. Concurrency - Master goroutines and channels for building high-performance concurrent systems
  3. Error Handling - Understand Go's explicit error handling patterns for robust, maintainable code
  4. Testing - Learn Go's testing framework for writing reliable, testable code
  5. Standard Library - Explore Go's comprehensive standard library for common programming tasks

Production Readiness

You now have the foundation for building production-ready Go applications. The patterns and concepts covered here are used extensively in:

  • Web services and REST APIs
  • Microservices architectures
  • Database systems and ORMs
  • Command-line tools and utilities
  • Game development and graphics systems

Remember: Go's strength lies in simplicity and composition. Master structs and methods, and you'll write clean, efficient, and maintainable Go code.