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 namejson:"field_name,omitempty"- Omit if zero valuejson:"-"- Never serializedb:"column_name"- Database column namevalidate:"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:
- Interfaces - Learn how Go achieves polymorphism with interfaces, enabling flexible APIs and dependency injection
- Concurrency - Master goroutines and channels for building high-performance concurrent systems
- Error Handling - Understand Go's explicit error handling patterns for robust, maintainable code
- Testing - Learn Go's testing framework for writing reliable, testable code
- 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.