Go Structs and Interfaces - Building Custom Types and Contracts

Learning Objectives: By the end of this lesson, you will be able to:

  • Design and implement custom struct types for complex data modeling
  • Master struct embedding for composition-based inheritance
  • Understand and implement Go’s implicit interface system
  • Apply interface-driven design for flexible, testable code
  • Use type assertions and type switches effectively
  • Build polymorphic systems using interfaces
  • Design clean APIs using interface contracts
  • Implement common design patterns with structs and interfaces

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Imagine you’re designing a video game. You have different types of characters (warriors, mages, archers), each with unique abilities, but they all share common traits like health and movement. How would you organize this without repeating code? Go’s structs and interfaces provide an elegant solution - structs let you model complex entities, while interfaces define contracts for behavior. What would happen if you could make your code both flexible and type-safe?

Lesson Overview

  • Duration: 10-12 hours
  • Prerequisites: Go functions, methods, arrays, slices, and maps
  • Materials Needed: Go development environment, understanding of basic OOP concepts

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Struct Fundamentals - Custom types and data modeling
  2. Methods on Structs - Attaching behavior to data
  3. Struct Embedding - Composition over inheritance
  4. Interface Basics - Defining contracts and implicit implementation
  5. Polymorphism - Working with different types through common interfaces
  6. Advanced Interface Patterns - Empty interfaces, type assertions, and switches
  7. Real-world Design - Building flexible, maintainable systems
  8. Best Practices - Interface design principles and common patterns

Success Criteria:

By the end of this session, you should be able to design complex data models using structs, create flexible APIs with interfaces, and understand Go’s unique approach to object-oriented programming.

3. Foundation Knowledge (15-20 minutes)

Understanding Go’s Type System Philosophy

Go takes a different approach to object-oriented programming compared to traditional OOP languages. Instead of classes and inheritance, Go uses structs and composition, with interfaces providing the abstraction layer.

Real-World Analogy: Think of structs like blueprints for building different types of vehicles (car, truck, motorcycle). Each blueprint defines the structure and components. Interfaces are like driver’s licenses - they define what capabilities something must have to be “drivable” (start, stop, turn), regardless of whether it’s a car or motorcycle.

Why This Matters: Go’s approach provides:

  • Composition over Inheritance: More flexible than traditional class hierarchies
  • Implicit Interfaces: No explicit “implements” keyword - duck typing with compile-time safety
  • Zero-cost Abstractions: Interfaces compile to efficient code
  • Testability: Easy to mock and test through interfaces
  • Decoupling: Code depends on behavior, not concrete types

Core Concepts

Structs as Data Containers

  • Custom Types: Group related data together
  • Methods: Attach behavior to struct types
  • Embedding: Include other structs for composition
  • Value vs Pointer Semantics: Control how data is passed and modified

Interfaces as Behavior Contracts

  • Implicit Implementation: Types satisfy interfaces automatically
  • Polymorphism: Different types can be used interchangeably
  • Interface Segregation: Small, focused interfaces are preferred
  • Empty Interface: interface{} can hold any type

Go’s OOP Principles

  1. Favor composition over inheritance
  2. Design interfaces, not implementations
  3. Keep interfaces small and focused
  4. Accept interfaces, return concrete types

Key Terminology

  • Struct: Custom type that groups related data fields
  • Field: Individual data element within a struct
  • Method Set: Collection of methods associated with a type
  • Embedding: Including one struct inside another
  • Interface: Contract defining required methods
  • Satisfying an Interface: Type has all methods required by interface
  • Type Assertion: Extracting concrete type from interface
  • Polymorphism: Using different types through common interface

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Go’s Unique Approach to OOP

Go’s design philosophy emphasizes simplicity and composition. Unlike traditional OOP languages with complex inheritance hierarchies, Go uses:

Composition over Inheritance

Instead of “is-a” relationships (inheritance), Go encourages “has-a” relationships (composition). This leads to more flexible and maintainable code.

Interface Satisfaction

Go’s interfaces are satisfied implicitly. If a type has all the methods an interface requires, it automatically satisfies that interface. This enables:

  • Retroactive Interface Satisfaction: Add interfaces to existing types
  • Multiple Interface Satisfaction: One type can satisfy many interfaces
  • Decoupled Design: Interfaces can be defined where they’re used

Practical Applications

Structs and interfaces are essential for:

  • API Design: Clean, testable HTTP handlers and services
  • Database Modeling: Representing entities and their relationships
  • Plugin Systems: Defining contracts for extensible applications
  • Microservices: Standardizing communication between services
  • Testing: Creating mocks and test doubles

Common Misconceptions

  1. Misconception: Go doesn’t support object-oriented programming Reality: Go supports OOP through structs, methods, and interfaces

  2. Misconception: Embedding is inheritance Reality: Embedding is composition - the embedded type is a field

  3. Misconception: Interfaces must be large and comprehensive Reality: Go favors small, focused interfaces (often just 1-2 methods)

5. Live Coding Demonstration (20-25 minutes)

Example 1: Struct Fundamentals and Methods

package main

import (
    "fmt"
    "math"
    "time"
)

// Basic struct definition
type Person struct {
    FirstName string
    LastName  string
    Age       int
    Email     string
}

// Struct with different field types
type BankAccount struct {
    AccountNumber string
    Balance       float64
    Owner         Person    // Embedded struct
    CreatedAt     time.Time
    IsActive      bool
}

// Struct with tags (useful for JSON, database mapping)
type Product struct {
    ID          int     `json:"id" db:"product_id"`
    Name        string  `json:"name" db:"product_name"`
    Price       float64 `json:"price" db:"price"`
    Description string  `json:"description,omitempty" db:"description"`
}

// Methods with value receivers
func (p Person) FullName() string {
    return p.FirstName + " " + p.LastName
}

func (p Person) IsAdult() bool {
    return p.Age >= 18
}

func (p Person) Greet() string {
    return fmt.Sprintf("Hello, I'm %s", p.FullName())
}

// Methods with pointer receivers (can modify the receiver)
func (b *BankAccount) Deposit(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("deposit amount must be positive")
    }
    b.Balance += amount
    return nil
}

func (b *BankAccount) Withdraw(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("withdrawal amount must be positive")
    }
    if amount > b.Balance {
        return fmt.Errorf("insufficient funds")
    }
    b.Balance -= amount
    return nil
}

func (b BankAccount) GetBalance() float64 {
    return b.Balance
}

// Method that returns multiple values
func (b BankAccount) AccountInfo() (string, float64, bool) {
    return b.AccountNumber, b.Balance, b.IsActive
}

func main() {
    fmt.Println("=== Struct Creation and Usage ===")
    
    // Different ways to create structs
    // Method 1: Field by field
    var person1 Person
    person1.FirstName = "John"
    person1.LastName = "Doe"
    person1.Age = 30
    person1.Email = "john@example.com"
    
    // Method 2: Struct literal with field names
    person2 := Person{
        FirstName: "Jane",
        LastName:  "Smith",
        Age:       25,
        Email:     "jane@example.com",
    }
    
    // Method 3: Struct literal without field names (not recommended)
    person3 := Person{"Bob", "Johnson", 35, "bob@example.com"}
    
    // Method 4: Partial initialization
    person4 := Person{
        FirstName: "Alice",
        LastName:  "Williams",
        // Age and Email will be zero values
    }
    
    fmt.Printf("Person1: %+v\n", person1)
    fmt.Printf("Person2: %+v\n", person2)
    fmt.Printf("Person3: %+v\n", person3)
    fmt.Printf("Person4: %+v\n", person4)
    
    // Using methods
    fmt.Println("\n=== Method Usage ===")
    fmt.Printf("%s\n", person1.Greet())
    fmt.Printf("Full name: %s\n", person2.FullName())
    fmt.Printf("Is adult: %t\n", person2.IsAdult())
    
    // Bank account example
    fmt.Println("\n=== Bank Account Example ===")
    account := BankAccount{
        AccountNumber: "ACC123456",
        Balance:       1000.0,
        Owner:         person1,
        CreatedAt:     time.Now(),
        IsActive:      true,
    }
    
    fmt.Printf("Initial account: %+v\n", account)
    fmt.Printf("Owner: %s\n", account.Owner.FullName())
    
    // Deposit money
    if err := account.Deposit(500.0); err != nil {
        fmt.Printf("Deposit error: %s\n", err)
    } else {
        fmt.Printf("After deposit: Balance = %.2f\n", account.GetBalance())
    }
    
    // Withdraw money
    if err := account.Withdraw(200.0); err != nil {
        fmt.Printf("Withdrawal error: %s\n", err)
    } else {
        fmt.Printf("After withdrawal: Balance = %.2f\n", account.GetBalance())
    }
    
    // Try to withdraw too much
    if err := account.Withdraw(2000.0); err != nil {
        fmt.Printf("Withdrawal error: %s\n", err)
    }
    
    // Get account info
    accNum, balance, active := account.AccountInfo()
    fmt.Printf("Account: %s, Balance: %.2f, Active: %t\n", accNum, balance, active)
}

Explanation:

  • Lines 8-13: Basic struct with various field types
  • Lines 15-21: Struct composition with embedded Person
  • Lines 24-28: Struct tags for metadata (JSON, database mapping)
  • Lines 31-39: Methods with value receivers - work on copies
  • Lines 42-56: Methods with pointer receivers - can modify original
  • Lines 90-106: Different struct initialization patterns

Example 2: Struct Embedding and Composition

package main

import (
    "fmt"
    "time"
)

// Base types
type Address struct {
    Street   string
    City     string
    State    string
    ZipCode  string
    Country  string
}

type ContactInfo struct {
    Email string
    Phone string
}

// Embedding example
type Employee struct {
    ID           int
    FirstName    string
    LastName     string
    Address      Address     // Regular field
    ContactInfo              // Anonymous field (embedded)
    Department   string
    Salary       float64
    HireDate     time.Time
}

// Manager embeds Employee
type Manager struct {
    Employee                 // Embedded Employee
    TeamSize    int
    Budget      float64
    Reports     []Employee
}

// Methods on embedded types
func (a Address) FullAddress() string {
    return fmt.Sprintf("%s, %s, %s %s, %s",
        a.Street, a.City, a.State, a.ZipCode, a.Country)
}

func (c ContactInfo) PrimaryContact() string {
    if c.Email != "" {
        return c.Email
    }
    return c.Phone
}

// Methods on Employee
func (e Employee) FullName() string {
    return e.FirstName + " " + e.LastName
}

func (e Employee) YearsOfService() int {
    return int(time.Since(e.HireDate).Hours() / 24 / 365)
}

// Methods on Manager
func (m Manager) AddReport(emp Employee) {
    m.Reports = append(m.Reports, emp)
}

func (m Manager) TeamInfo() string {
    return fmt.Sprintf("Manager %s leads %d people with budget $%.2f",
        m.FullName(), m.TeamSize, m.Budget)
}

// Demonstrating method promotion
func (m Manager) GetContactInfo() string {
    // This calls the embedded ContactInfo's method
    return m.PrimaryContact()
}

// Interface for different shapes
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Rectangle struct
type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Circle struct
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Function that works with any Shape
func PrintShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    fmt.Println("=== Struct Embedding ===")
    
    // Create an employee
    emp := Employee{
        ID:        1001,
        FirstName: "John",
        LastName:  "Doe",
        Address: Address{
            Street:  "123 Main St",
            City:    "Anytown",
            State:   "CA",
            ZipCode: "12345",
            Country: "USA",
        },
        ContactInfo: ContactInfo{
            Email: "john.doe@company.com",
            Phone: "+1-555-0123",
        },
        Department: "Engineering",
        Salary:     75000.0,
        HireDate:   time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC),
    }
    
    fmt.Printf("Employee: %s\n", emp.FullName())
    fmt.Printf("Address: %s\n", emp.Address.FullAddress())
    
    // Accessing embedded fields directly (field promotion)
    fmt.Printf("Email: %s\n", emp.Email)  // Same as emp.ContactInfo.Email
    fmt.Printf("Phone: %s\n", emp.Phone)  // Same as emp.ContactInfo.Phone
    fmt.Printf("Primary contact: %s\n", emp.PrimaryContact())
    fmt.Printf("Years of service: %d\n", emp.YearsOfService())
    
    // Create a manager
    fmt.Println("\n=== Manager with Embedded Employee ===")
    manager := Manager{
        Employee: emp,  // Embedding an employee
        TeamSize: 5,
        Budget:   500000.0,
    }
    
    // Manager has access to all Employee methods and fields
    fmt.Printf("Manager: %s\n", manager.FullName())
    fmt.Printf("Manager department: %s\n", manager.Department)
    fmt.Printf("Manager email: %s\n", manager.Email)
    fmt.Printf("%s\n", manager.TeamInfo())
    
    // Demonstrating polymorphism with interfaces
    fmt.Println("\n=== Interface Polymorphism ===")
    
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}
    
    fmt.Println("Rectangle:")
    PrintShapeInfo(rect)
    
    fmt.Println("Circle:")
    PrintShapeInfo(circle)
    
    // Slice of different shapes
    shapes := []Shape{rect, circle}
    
    fmt.Println("All shapes:")
    for i, shape := range shapes {
        fmt.Printf("Shape %d: ", i+1)
        PrintShapeInfo(shape)
    }
}

Example 3: Interface Mastery and Type Assertions

package main

import (
    "fmt"
    "io"
    "strings"
)

// Define various interfaces
type Writer interface {
    Write([]byte) (int, error)
}

type Reader interface {
    Read([]byte) (int, error)
}

type ReadWriter interface {
    Reader
    Writer
}

type Closer interface {
    Close() error
}

// Interface composition
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Custom writer implementation
type UpperCaseWriter struct {
    writer io.Writer
}

func (ucw UpperCaseWriter) Write(data []byte) (int, error) {
    upperData := strings.ToUpper(string(data))
    return ucw.writer.Write([]byte(upperData))
}

// Buffer that implements multiple interfaces
type Buffer struct {
    data     []byte
    position int
    closed   bool
}

func NewBuffer() *Buffer {
    return &Buffer{
        data: make([]byte, 0),
    }
}

func (b *Buffer) Write(data []byte) (int, error) {
    if b.closed {
        return 0, fmt.Errorf("buffer is closed")
    }
    b.data = append(b.data, data...)
    return len(data), nil
}

func (b *Buffer) Read(data []byte) (int, error) {
    if b.closed {
        return 0, fmt.Errorf("buffer is closed")
    }
    
    available := len(b.data) - b.position
    if available == 0 {
        return 0, io.EOF
    }
    
    n := copy(data, b.data[b.position:])
    b.position += n
    return n, nil
}

func (b *Buffer) Close() error {
    b.closed = true
    return nil
}

func (b *Buffer) String() string {
    return string(b.data)
}

func (b *Buffer) Reset() {
    b.data = b.data[:0]
    b.position = 0
    b.closed = false
}

// Different types implementing the same interface
type Animal interface {
    Speak() string
    Move() string
}

type Dog struct {
    Name string
    Breed string
}

func (d Dog) Speak() string {
    return "Woof!"
}

func (d Dog) Move() string {
    return "Running on four legs"
}

type Bird struct {
    Name    string
    Species string
}

func (b Bird) Speak() string {
    return "Tweet!"
}

func (b Bird) Move() string {
    return "Flying with wings"
}

type Fish struct {
    Name string
    Type string
}

func (f Fish) Speak() string {
    return "Blub blub"
}

func (f Fish) Move() string {
    return "Swimming with fins"
}

// Function that works with any Animal
func DescribeAnimal(a Animal) {
    fmt.Printf("The animal says: %s\n", a.Speak())
    fmt.Printf("Movement: %s\n", a.Move())
    
    // Type assertion to access specific methods
    switch v := a.(type) {
    case Dog:
        fmt.Printf("This is a %s dog named %s\n", v.Breed, v.Name)
    case Bird:
        fmt.Printf("This is a %s bird named %s\n", v.Species, v.Name)
    case Fish:
        fmt.Printf("This is a %s fish named %s\n", v.Type, v.Name)
    default:
        fmt.Printf("Unknown animal type: %T\n", v)
    }
}

// Empty interface example
func ProcessAnyValue(value interface{}) {
    fmt.Printf("Processing value: %v (type: %T)\n", value, value)
    
    // Type assertion with ok pattern
    if str, ok := value.(string); ok {
        fmt.Printf("  String length: %d\n", len(str))
    } else if num, ok := value.(int); ok {
        fmt.Printf("  Number squared: %d\n", num*num)
    } else if slice, ok := value.([]int); ok {
        fmt.Printf("  Slice sum: %d\n", sumSlice(slice))
    } else {
        fmt.Printf("  Unsupported type\n")
    }
}

func sumSlice(slice []int) int {
    sum := 0
    for _, v := range slice {
        sum += v
    }
    return sum
}

func main() {
    fmt.Println("=== Interface Implementation ===")
    
    // Using custom writer
    var buf strings.Builder
    upperWriter := UpperCaseWriter{writer: &buf}
    
    upperWriter.Write([]byte("Hello, World!"))
    fmt.Printf("Upper case output: %s\n", buf.String())
    
    // Using custom buffer
    fmt.Println("\n=== Custom Buffer ===")
    buffer := NewBuffer()
    
    // Buffer implements Writer
    buffer.Write([]byte("Hello, "))
    buffer.Write([]byte("Go World!"))
    fmt.Printf("Buffer content: %s\n", buffer.String())
    
    // Buffer implements Reader
    readData := make([]byte, 5)
    n, err := buffer.Read(readData)
    if err != nil {
        fmt.Printf("Read error: %s\n", err)
    } else {
        fmt.Printf("Read %d bytes: %s\n", n, string(readData[:n]))
    }
    
    // Buffer implements Closer
    buffer.Close()
    
    // Polymorphism with animals
    fmt.Println("\n=== Animal Polymorphism ===")
    animals := []Animal{
        Dog{Name: "Buddy", Breed: "Golden Retriever"},
        Bird{Name: "Tweety", Species: "Canary"},
        Fish{Name: "Nemo", Type: "Clownfish"},
    }
    
    for i, animal := range animals {
        fmt.Printf("\nAnimal %d:\n", i+1)
        DescribeAnimal(animal)
    }
    
    // Empty interface examples
    fmt.Println("\n=== Empty Interface ===")
    values := []interface{}{
        "Hello, Go!",
        42,
        []int{1, 2, 3, 4, 5},
        3.14,
        true,
    }
    
    for _, value := range values {
        ProcessAnyValue(value)
        fmt.Println()
    }
    
    // Type assertion examples
    fmt.Println("=== Type Assertions ===")
    var i interface{} = "Hello"
    
    // Safe type assertion
    if s, ok := i.(string); ok {
        fmt.Printf("String value: %s\n", s)
    } else {
        fmt.Println("Not a string")
    }
    
    // This would panic if we didn't check:
    // s := i.(int)  // panic: interface conversion
    
    // Type switch
    switch v := i.(type) {
    case string:
        fmt.Printf("String: %s (length: %d)\n", v, len(v))
    case int:
        fmt.Printf("Integer: %d\n", v)
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

Example 4: Real-World Design Patterns

package main

import (
    "fmt"
    "time"
)

// Repository pattern with interfaces
type User struct {
    ID       int
    Username string
    Email    string
    Created  time.Time
}

type UserRepository interface {
    Save(user *User) error
    FindByID(id int) (*User, error)
    FindByUsername(username string) (*User, error)
    Delete(id int) error
}

// In-memory implementation
type InMemoryUserRepository struct {
    users  map[int]*User
    nextID int
}

func NewInMemoryUserRepository() *InMemoryUserRepository {
    return &InMemoryUserRepository{
        users:  make(map[int]*User),
        nextID: 1,
    }
}

func (r *InMemoryUserRepository) Save(user *User) error {
    if user.ID == 0 {
        user.ID = r.nextID
        user.Created = time.Now()
        r.nextID++
    }
    r.users[user.ID] = user
    return nil
}

func (r *InMemoryUserRepository) FindByID(id int) (*User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, fmt.Errorf("user with ID %d not found", id)
    }
    return user, nil
}

func (r *InMemoryUserRepository) FindByUsername(username string) (*User, error) {
    for _, user := range r.users {
        if user.Username == username {
            return user, nil
        }
    }
    return nil, fmt.Errorf("user with username %s not found", username)
}

func (r *InMemoryUserRepository) Delete(id int) error {
    if _, exists := r.users[id]; !exists {
        return fmt.Errorf("user with ID %d not found", id)
    }
    delete(r.users, id)
    return nil
}

// Service layer using repository
type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) CreateUser(username, email string) (*User, error) {
    // Validation
    if username == "" {
        return nil, fmt.Errorf("username cannot be empty")
    }
    if email == "" {
        return nil, fmt.Errorf("email cannot be empty")
    }
    
    // Check if username already exists
    if _, err := s.repo.FindByUsername(username); err == nil {
        return nil, fmt.Errorf("username %s already exists", username)
    }
    
    user := &User{
        Username: username,
        Email:    email,
    }
    
    if err := s.repo.Save(user); err != nil {
        return nil, fmt.Errorf("failed to save user: %w", err)
    }
    
    return user, nil
}

func (s *UserService) GetUser(id int) (*User, error) {
    return s.repo.FindByID(id)
}

// Strategy pattern with interfaces
type PaymentProcessor interface {
    ProcessPayment(amount float64) error
    GetName() string
}

type CreditCardProcessor struct {
    CardNumber string
    CVV        string
}

func (cc CreditCardProcessor) ProcessPayment(amount float64) error {
    fmt.Printf("Processing $%.2f payment with credit card ending in %s\n", 
               amount, cc.CardNumber[len(cc.CardNumber)-4:])
    return nil
}

func (cc CreditCardProcessor) GetName() string {
    return "Credit Card"
}

type PayPalProcessor struct {
    Email string
}

func (pp PayPalProcessor) ProcessPayment(amount float64) error {
    fmt.Printf("Processing $%.2f payment via PayPal for %s\n", amount, pp.Email)
    return nil
}

func (pp PayPalProcessor) GetName() string {
    return "PayPal"
}

type Order struct {
    ID       int
    Amount   float64
    Processor PaymentProcessor
}

func (o *Order) ProcessPayment() error {
    fmt.Printf("Order %d: Using %s payment processor\n", o.ID, o.Processor.GetName())
    return o.Processor.ProcessPayment(o.Amount)
}

// Observer pattern
type EventListener interface {
    OnEvent(event string, data interface{})
}

type EmailNotifier struct {
    Email string
}

func (en EmailNotifier) OnEvent(event string, data interface{}) {
    fmt.Printf("Email notification to %s: %s - %v\n", en.Email, event, data)
}

type SMSNotifier struct {
    Phone string
}

func (sn SMSNotifier) OnEvent(event string, data interface{}) {
    fmt.Printf("SMS notification to %s: %s - %v\n", sn.Phone, event, data)
}

type EventManager struct {
    listeners []EventListener
}

func NewEventManager() *EventManager {
    return &EventManager{
        listeners: make([]EventListener, 0),
    }
}

func (em *EventManager) Subscribe(listener EventListener) {
    em.listeners = append(em.listeners, listener)
}

func (em *EventManager) Notify(event string, data interface{}) {
    for _, listener := range em.listeners {
        listener.OnEvent(event, data)
    }
}

func main() {
    // Repository pattern demo
    fmt.Println("=== Repository Pattern ===")
    
    repo := NewInMemoryUserRepository()
    service := NewUserService(repo)
    
    // Create users
    user1, err := service.CreateUser("john_doe", "john@example.com")
    if err != nil {
        fmt.Printf("Error creating user: %s\n", err)
    } else {
        fmt.Printf("Created user: %+v\n", user1)
    }
    
    user2, err := service.CreateUser("jane_smith", "jane@example.com")
    if err != nil {
        fmt.Printf("Error creating user: %s\n", err)
    } else {
        fmt.Printf("Created user: %+v\n", user2)
    }
    
    // Try to create duplicate
    _, err = service.CreateUser("john_doe", "john2@example.com")
    if err != nil {
        fmt.Printf("Expected error: %s\n", err)
    }
    
    // Retrieve user
    retrievedUser, err := service.GetUser(1)
    if err != nil {
        fmt.Printf("Error retrieving user: %s\n", err)
    } else {
        fmt.Printf("Retrieved user: %+v\n", retrievedUser)
    }
    
    // Strategy pattern demo
    fmt.Println("\n=== Strategy Pattern ===")
    
    creditCard := CreditCardProcessor{
        CardNumber: "1234567890123456",
        CVV:        "123",
    }
    
    paypal := PayPalProcessor{
        Email: "user@example.com",
    }
    
    orders := []*Order{
        {ID: 1, Amount: 99.99, Processor: creditCard},
        {ID: 2, Amount: 149.99, Processor: paypal},
    }
    
    for _, order := range orders {
        order.ProcessPayment()
    }
    
    // Observer pattern demo
    fmt.Println("\n=== Observer Pattern ===")
    
    eventManager := NewEventManager()
    
    emailNotifier := EmailNotifier{Email: "admin@company.com"}
    smsNotifier := SMSNotifier{Phone: "+1-555-0123"}
    
    eventManager.Subscribe(emailNotifier)
    eventManager.Subscribe(smsNotifier)
    
    // Trigger events
    eventManager.Notify("user_registered", map[string]interface{}{
        "username": "new_user",
        "email":    "new_user@example.com",
    })
    
    eventManager.Notify("order_completed", map[string]interface{}{
        "order_id": 12345,
        "amount":   299.99,
    })
}

Debugging Demonstration

Show common struct and interface errors and solutions:

Problem: Method on nil receiver

type Account struct {
    Balance float64
}

func (a *Account) Deposit(amount float64) {
    a.Balance += amount  // Panic if a is nil
}

// Safe approach
func (a *Account) SafeDeposit(amount float64) error {
    if a == nil {
        return fmt.Errorf("account is nil")
    }
    a.Balance += amount
    return nil
}

Problem: Interface satisfaction confusion

// This won't compile if Rectangle doesn't satisfy Shape
// var shape Shape = Rectangle{Width: 10, Height: 5}

// Check interface satisfaction at compile time
var _ Shape = Rectangle{}  // Compile error if Rectangle doesn't satisfy Shape

6. Hands-On Practice (30-40 minutes)

Exercise 1: Library Management System (10 minutes)

Objective: Design a library system using structs and methods

Instructions:

  1. Create structs for Book, Author, and Library
  2. Implement methods for borrowing and returning books
  3. Add validation and error handling
  4. Track borrowing history

Starter Code:

package main

import (
    "fmt"
    "time"
)

// TODO: Define Book struct with title, author, ISBN, available status
// TODO: Define Author struct with name, birth year, biography
// TODO: Define Library struct with books collection
// TODO: Implement methods for book operations

func main() {
    // Test your library system here
}

Solution:

package main

import (
    "fmt"
    "time"
)

type Author struct {
    Name      string
    BirthYear int
    Biography string
}

func (a Author) String() string {
    return fmt.Sprintf("%s (born %d)", a.Name, a.BirthYear)
}

type Book struct {
    ID          int
    Title       string
    Author      Author
    ISBN        string
    Available   bool
    BorrowedBy  string
    BorrowedAt  time.Time
    ReturnedAt  time.Time
}

func (b Book) String() string {
    status := "Available"
    if !b.Available {
        status = fmt.Sprintf("Borrowed by %s", b.BorrowedBy)
    }
    return fmt.Sprintf("%s by %s [%s]", b.Title, b.Author.Name, status)
}

type Library struct {
    Name  string
    Books map[int]*Book
    nextID int
}

func NewLibrary(name string) *Library {
    return &Library{
        Name:   name,
        Books:  make(map[int]*Book),
        nextID: 1,
    }
}

func (l *Library) AddBook(title string, author Author, isbn string) *Book {
    book := &Book{
        ID:        l.nextID,
        Title:     title,
        Author:    author,
        ISBN:      isbn,
        Available: true,
    }
    l.Books[l.nextID] = book
    l.nextID++
    return book
}

func (l *Library) BorrowBook(bookID int, borrower string) error {
    book, exists := l.Books[bookID]
    if !exists {
        return fmt.Errorf("book with ID %d not found", bookID)
    }
    
    if !book.Available {
        return fmt.Errorf("book is already borrowed by %s", book.BorrowedBy)
    }
    
    book.Available = false
    book.BorrowedBy = borrower
    book.BorrowedAt = time.Now()
    
    return nil
}

func (l *Library) ReturnBook(bookID int) error {
    book, exists := l.Books[bookID]
    if !exists {
        return fmt.Errorf("book with ID %d not found", bookID)
    }
    
    if book.Available {
        return fmt.Errorf("book is not currently borrowed")
    }
    
    book.Available = true
    book.ReturnedAt = time.Now()
    borrower := book.BorrowedBy
    book.BorrowedBy = ""
    
    fmt.Printf("Book returned by %s after %v\n", borrower, 
               book.ReturnedAt.Sub(book.BorrowedAt))
    
    return nil
}

func (l *Library) ListBooks() {
    fmt.Printf("Books in %s:\n", l.Name)
    for _, book := range l.Books {
        fmt.Printf("  %d: %s\n", book.ID, book.String())
    }
}

func main() {
    library := NewLibrary("City Central Library")
    
    // Add authors
    tolkien := Author{
        Name:      "J.R.R. Tolkien",
        BirthYear: 1892,
        Biography: "English author and professor",
    }
    
    orwell := Author{
        Name:      "George Orwell",
        BirthYear: 1903,
        Biography: "English novelist and journalist",
    }
    
    // Add books
    book1 := library.AddBook("The Hobbit", tolkien, "978-0547928227")
    book2 := library.AddBook("1984", orwell, "978-0451524935")
    
    fmt.Printf("Added books:\n")
    fmt.Printf("  %s\n", book1)
    fmt.Printf("  %s\n", book2)
    
    // List all books
    fmt.Println()
    library.ListBooks()
    
    // Borrow a book
    fmt.Println("\nBorrowing The Hobbit...")
    if err := library.BorrowBook(1, "Alice Johnson"); err != nil {
        fmt.Printf("Error: %s\n", err)
    } else {
        fmt.Println("Book borrowed successfully!")
    }
    
    // Try to borrow the same book again
    fmt.Println("\nTrying to borrow The Hobbit again...")
    if err := library.BorrowBook(1, "Bob Smith"); err != nil {
        fmt.Printf("Error: %s\n", err)
    }
    
    // List books after borrowing
    fmt.Println()
    library.ListBooks()
    
    // Return the book
    fmt.Println("\nReturning The Hobbit...")
    if err := library.ReturnBook(1); err != nil {
        fmt.Printf("Error: %s\n", err)
    } else {
        fmt.Println("Book returned successfully!")
    }
    
    // Final book list
    fmt.Println()
    library.ListBooks()
}

Exercise 2: Shape Calculator with Interfaces (15 minutes)

Objective: Create a shape calculation system demonstrating interface polymorphism

Requirements:

  • Define a Shape interface with area and perimeter methods
  • Implement different shapes (Rectangle, Circle, Triangle)
  • Create a ShapeCalculator that works with any Shape
  • Add specialized methods for specific shapes

Instructions:

  1. Design a clean Shape interface
  2. Implement multiple shape types
  3. Create functions that work with the interface
  4. Add type-specific functionality using type assertions

Hints:

  • Use math package for geometric calculations
  • Consider adding a Name() method to the interface
  • Think about validation for shape dimensions

Exercise 3: Plugin System with Interfaces (15 minutes)

Objective: Build a plugin architecture using interfaces

Challenge: Create an extensible system that can load different types of processors

Requirements:

  • Define Plugin interface with common methods
  • Implement different plugin types (TextProcessor, ImageProcessor, DataProcessor)
  • Create a PluginManager to register and execute plugins
  • Support plugin discovery and execution

Extension Ideas:

  • Add plugin metadata and versioning
  • Implement plugin dependencies
  • Create a configuration system for plugins
  • Add plugin lifecycle management (start, stop, health checks)

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Using value receivers when pointer receivers are needed Cause: Not understanding when methods should modify the receiver Solution: Use pointer receivers for modifications or large structs

  2. Problem: Interface{} overuse leading to runtime errors Cause: Losing type safety by using empty interfaces everywhere Solution: Use specific interfaces; employ empty interface sparingly

  3. Problem: Large interface definitions Cause: Trying to create comprehensive interfaces like other languages Solution: Follow Go’s principle of small, focused interfaces

Debugging Tips

  • Use go vet to catch common interface satisfaction issues
  • Check nil pointers before calling methods on pointer receivers
  • Use type assertions safely with the comma ok idiom
  • Print types with fmt.Printf("%T", variable) when debugging interfaces

Design Best Practices

  • Accept interfaces, return concrete types in function signatures
  • Keep interfaces small - prefer many small interfaces over few large ones
  • Define interfaces where they’re used not where they’re implemented
  • Use embedding for composition rather than trying to simulate inheritance

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Interface Naming: Use “-er” suffix for single-method interfaces (Reader, Writer, Closer)
  • Struct Initialization: Always use named field initialization for public APIs
  • Method Receivers: Use pointer receivers for large structs or when modification is needed
  • Interface Design: Design interfaces at the boundary where they’re needed

Code Organization

// Good: Small, focused interfaces
type Reader interface {
    Read([]byte) (int, error)
}

type Writer interface {
    Write([]byte) (int, error)
}

// Good: Interface composition
type ReadWriter interface {
    Reader
    Writer
}

// Good: Accepting interfaces
func ProcessData(r Reader, w Writer) error {
    // Implementation
}

Performance Considerations

  • Struct layout: Order fields by size for better memory alignment
  • Interface calls: Slight overhead compared to direct method calls
  • Embedding depth: Deep embedding can impact performance
  • Type assertions: Use sparingly in hot paths

Next Steps in Your Learning Journey

  • Concurrency: Learn how interfaces enable concurrent programming patterns
  • Testing: Use interfaces for dependency injection and mocking
  • Reflection: Understand how Go’s reflection works with interfaces
  • Design Patterns: Study how classic patterns apply in Go

9. Summary and Review (5 minutes)

Key Takeaways

  1. Structs: Custom types that group related data and behavior
  2. Embedding: Composition mechanism that provides inheritance-like functionality
  3. Interfaces: Contracts that enable polymorphism and decoupled design
  4. Implicit Satisfaction: Types automatically satisfy interfaces they implement

Quick Review Questions

  1. What’s the difference between embedding and regular struct fields?
  2. When should you use a pointer receiver vs value receiver for methods?
  3. How does Go’s interface system differ from traditional OOP languages?
  4. What’s the purpose of the empty interface, and when should you use it?

Answers:

  1. Embedding promotes fields/methods to the outer type; regular fields require explicit access
  2. Use pointer receivers for modifications, large structs, or consistency across method sets
  3. Go uses implicit satisfaction (duck typing) with compile-time checking, no inheritance
  4. Empty interface holds any type; use sparingly when you truly need to work with any type

For Instructors

Teaching Tips

  • Use visual diagrams to show struct memory layout and interface relationships
  • Demonstrate composition benefits by comparing with inheritance-based solutions
  • Show interface satisfaction at compile time vs runtime
  • Emphasize small interfaces and their composition

Assessment Ideas

  • Design challenges: Present problems requiring appropriate struct/interface design
  • Refactoring exercises: Convert inheritance-based code to Go’s composition model
  • Interface satisfaction: Given structs and interfaces, determine which satisfy which
  • Real-world modeling: Model complex domains using Go’s type system

Adaptation Notes

  • Beginner Focus: Start with simple structs before moving to interfaces
  • Advanced Students: Explore interface internals and performance implications
  • OOP Background: Help students unlearn inheritance patterns
  • Time Management: Interface concepts can be abstract - use concrete examples

Common Student Questions

  1. “Why doesn’t Go have inheritance?” - Composition is more flexible and clearer
  2. “When should I use interfaces?” - When you need polymorphism or want to decouple code
  3. “Can I have optional interface methods?” - No, but you can compose smaller interfaces
  4. “How do I know if my type satisfies an interface?” - Compiler checks, or use var _ Interface = Type{}