Interfaces

Why This Matters

Consider building a payment processing system that needs to support credit cards, PayPal, Bitcoin, and future payment methods you haven't even thought of yet. Writing separate code for each payment method would mean duplicating logic and creating maintenance nightmares.

This is exactly the problem interfaces solve in Go. They allow you to write code that works with any type that satisfies certain behaviors, making your code flexible, testable, and future-proof.

Real-World Impact:

  • Plugin Systems: Load different implementations at runtime
  • Testing: Replace real services with mocks for reliable tests
  • API Design: Create stable contracts that outlive implementations
  • Team Collaboration: Different teams can work on different implementations independently

Interfaces are Go's superpower for building maintainable, extensible systems. Master them, and you'll write code that adapts to change instead of breaking under it.

Learning Objectives

By the end of this article, you'll master:

  1. Interface Philosophy: Understand Go's revolutionary implicit satisfaction approach
  2. Interface Design: Create small, focused interfaces that are easy to implement
  3. Polymorphism: Write functions that work with multiple types seamlessly
  4. Best Practices: Apply "Accept interfaces, return structs" principle effectively
  5. Testing Patterns: Use interfaces to create testable, mock-friendly code
  6. Advanced Patterns: Master interface composition and dependency injection

Core Concepts - Understanding Go's Interface Philosophy

The Go Revolution: Implicit Satisfaction

Unlike languages like Java where you explicitly declare implements InterfaceName, Go uses structural typing - if a type has all the methods an interface requires, it automatically satisfies that interface.

This is revolutionary because:

  • No import cycles between interface and implementation packages
  • You can create interfaces for existing types without modifying them
  • Third-party types can satisfy your interfaces automatically
  • Encourages small, focused interfaces
 1// run
 2package main
 3
 4import "fmt"
 5
 6// Define an interface - our contract
 7type Speaker interface {
 8	Speak() string
 9}
10
11// Dog implements Speaker
12type Dog struct {
13	Name string
14}
15
16func (d Dog) Speak() string {
17	return "Woof! I'm " + d.Name
18}
19
20// Cat implements Speaker
21type Cat struct {
22	Name string
23}
24
25func (c Cat) Speak() string {
26	return "Meow! I'm " + c.Name
27}
28
29// Function that works with ANY Speaker
30func introduce(s Speaker) {
31	fmt.Println(s.Speak())
32}
33
34func main() {
35	// Both Dog and Cat automatically satisfy Speaker
36	introduce(Dog{Name: "Buddy"})
37	introduce(Cat{Name: "Whiskers"})
38}

What's happening:

  1. Speaker interface requires Speak() string method
  2. Dog has that method → Dog automatically satisfies Speaker
  3. Cat has that method → Cat automatically satisfies Speaker
  4. No explicit declaration needed!

Interface Values: What's Under the Hood?

An interface value holds two things:

  1. Type information
  2. Concrete value
 1// run
 2package main
 3
 4import "fmt"
 5
 6type Speaker interface {
 7	Speak() string
 8}
 9
10type Dog struct {
11	Name string
12}
13
14func (d Dog) Speak() string {
15	return "Woof! I'm " + d.Name
16}
17
18func main() {
19	var s Speaker // s is nil interface value
20
21	dog := Dog{Name: "Buddy"}
22	s = dog // s now holds Dog type and Dog value
23
24	fmt.Printf("Type: %T\n", s)  // main.Dog
25	fmt.Printf("Value: %v\n", s) // {Buddy}
26
27	// Method call uses dynamic dispatch
28	fmt.Println(s.Speak()) // Calls Dog.Speak()
29}

Key Insight: When you call s.Speak(), Go looks up the type stored in s and calls the appropriate method. This is called dynamic dispatch.

Practical Examples - From Basic to Advanced

Example 1: Basic Polymorphism

Let's build a simple shape calculator that works with different shapes:

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"math"
 7)
 8
 9// Shape interface defines what all shapes must do
10type Shape interface {
11	Area() float64
12	Perimeter() float64
13}
14
15// Rectangle implements Shape
16type Rectangle struct {
17	Width, Height float64
18}
19
20func (r Rectangle) Area() float64 {
21	return r.Width * r.Height
22}
23
24func (r Rectangle) Perimeter() float64 {
25	return 2 * (r.Width + r.Height)
26}
27
28// Circle implements Shape
29type Circle struct {
30	Radius float64
31}
32
33func (c Circle) Area() float64 {
34	return math.Pi * c.Radius * c.Radius
35}
36
37func (c Circle) Perimeter() float64 {
38	return 2 * math.Pi * c.Radius
39}
40
41// Triangle implements Shape
42type Triangle struct {
43	Base, Height float64
44}
45
46func (t Triangle) Area() float64 {
47	return 0.5 * t.Base * t.Height
48}
49
50func (t Triangle) Perimeter() float64 {
51	// Simplified: assuming isosceles triangle
52	side := math.Sqrt(t.Base*t.Base/4 + t.Height*t.Height)
53	return t.Base + 2*side
54}
55
56// Function works with ANY Shape
57func printShapeInfo(s Shape, name string) {
58	fmt.Printf("%s:\n", name)
59	fmt.Printf("  Area: %.2f\n", s.Area())
60	fmt.Printf("  Perimeter: %.2f\n", s.Perimeter())
61	fmt.Println()
62}
63
64func main() {
65	shapes := []Shape{
66		Rectangle{Width: 10, Height: 5},
67		Circle{Radius: 7},
68		Triangle{Base: 6, Height: 8},
69	}
70
71	for i, shape := range shapes {
72		printShapeInfo(shape, fmt.Sprintf("Shape %d", i+1))
73	}
74}

The Power: You can add new shapes and printShapeInfo works with them automatically - no changes needed!

Example 2: Interface Composition

Build complex interfaces from simple ones:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Basic interfaces
 7type Reader interface {
 8	Read() string
 9}
10
11type Writer interface {
12	Write(data string)
13}
14
15type Closer interface {
16	Close() error
17}
18
19// Composed interface
20type ReadWriteCloser interface {
21	Reader
22	Writer
23	Closer
24}
25
26// Simple file implementation
27type File struct {
28	content string
29	closed  bool
30}
31
32func (f *File) Read() string {
33	if f.closed {
34		return ""
35	}
36	return f.content
37}
38
39func (f *File) Write(data string) {
40	if f.closed {
41		return
42	}
43	f.content += data
44}
45
46func (f *File) Close() error {
47	f.closed = true
48	fmt.Printf("File closed. Final content: %s\n", f.content)
49	return nil
50}
51
52// Function works with any ReadWriteCloser
53func processFile(rwc ReadWriteCloser) {
54	data := rwc.Read()
55	fmt.Printf("Read: %s\n", data)
56
57	rwc.Write(" - modified")
58	fmt.Printf("After write: %s\n", rwc.Read())
59
60	rwc.Close()
61}
62
63func main() {
64	file := &File{content: "Hello World"}
65
66	// File automatically satisfies ReadWriteCloser
67	// because it has Read(), Write(), and Close() methods
68	processFile(file)
69}

Key Benefit: Small interfaces are easy to implement and can be combined as needed.

Example 3: Dependency Injection for Testing

This is where interfaces truly shine in real applications:

  1// run
  2package main
  3
  4import "fmt"
  5
  6// Database interface defines what we need from a database
  7type Database interface {
  8	GetUser(id int) (*User, error)
  9	SaveUser(user *User) error
 10}
 11
 12// User model
 13type User struct {
 14	ID    int
 15	Name  string
 16	Email string
 17}
 18
 19// Production implementation
 20type RealDatabase struct {
 21	connectionString string
 22}
 23
 24func (db *RealDatabase) GetUser(id int) (*User, error) {
 25	// In real code: SQL query to database
 26	return &User{ID: id, Name: "Real User", Email: "real@example.com"}, nil
 27}
 28
 29func (db *RealDatabase) SaveUser(user *User) error {
 30	// In real code: INSERT or UPDATE in database
 31	fmt.Printf("Saving to real database: %+v\n", user)
 32	return nil
 33}
 34
 35// Mock implementation for testing
 36type MockDatabase struct {
 37	users map[int]*User
 38}
 39
 40func NewMockDatabase() *MockDatabase {
 41	return &MockDatabase{
 42		users: make(map[int]*User),
 43	}
 44}
 45
 46func (db *MockDatabase) GetUser(id int) (*User, error) {
 47	user, exists := db.users[id]
 48	if !exists {
 49		return nil, fmt.Errorf("user not found")
 50	}
 51	return user, nil
 52}
 53
 54func (db *MockDatabase) SaveUser(user *User) error {
 55	db.users[user.ID] = user
 56	fmt.Printf("Saved to mock database: %+v\n", user)
 57	return nil
 58}
 59
 60// Service that depends on Database interface
 61type UserService struct {
 62	db Database
 63}
 64
 65func NewUserService(db Database) *UserService {
 66	return &UserService{db: db}
 67}
 68
 69func (s *UserService) UpdateUserEmail(id int, newEmail string) error {
 70	user, err := s.db.GetUser(id)
 71	if err != nil {
 72		return err
 73	}
 74
 75	user.Email = newEmail
 76	return s.db.SaveUser(user)
 77}
 78
 79// Test function using mock
 80func TestUpdateUserEmail() {
 81	// Use mock database for testing
 82	mockDB := NewMockDatabase()
 83	mockDB.SaveUser(&User{ID: 1, Name: "Test User", Email: "old@example.com"})
 84
 85	service := NewUserService(mockDB)
 86
 87	err := service.UpdateUserEmail(1, "new@example.com")
 88	if err != nil {
 89		fmt.Printf("Test failed: %v\n", err)
 90	} else {
 91		fmt.Println("Test passed!")
 92	}
 93
 94	// Verify the update
 95	updated, err := mockDB.GetUser(1)
 96	if err == nil && updated.Email == "new@example.com" {
 97		fmt.Printf("Verified: %+v\n", updated)
 98	}
 99}
100
101func main() {
102	fmt.Println("=== Production Mode ===")
103
104	// Real database
105	realDB := &RealDatabase{connectionString: "postgresql://..."}
106	userService := NewUserService(realDB)
107
108	user, err := userService.db.GetUser(1)
109	if err == nil {
110		fmt.Printf("Real user: %+v\n", user)
111	}
112
113	fmt.Println("\n=== Testing Mode ===")
114	TestUpdateUserEmail()
115}

The Magic: The same UserService works with both real and mock databases without any changes! This is the power of programming to interfaces, not implementations.

Interface Design Patterns and Best Practices

Pattern 1: Small Interface Principle

The smaller the interface, the easier it is to implement and test:

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"time"
 7)
 8
 9// Small, focused interfaces are best
10type Logger interface {
11	Log(message string)
12}
13
14type TimestampLogger interface {
15	LogWithTimestamp(message string)
16}
17
18// ConsoleLogger implements Logger
19type ConsoleLogger struct{}
20
21func (l *ConsoleLogger) Log(message string) {
22	fmt.Println("[LOG]", message)
23}
24
25// TimedConsoleLogger implements both interfaces
26type TimedConsoleLogger struct{}
27
28func (l *TimedConsoleLogger) Log(message string) {
29	fmt.Println("[LOG]", message)
30}
31
32func (l *TimedConsoleLogger) LogWithTimestamp(message string) {
33	fmt.Printf("[%s] %s\n", time.Now().Format("15:04:05"), message)
34}
35
36// Functions depend on minimal interfaces
37func logMessage(logger Logger, msg string) {
38	logger.Log(msg)
39}
40
41func logImportantMessage(logger TimestampLogger, msg string) {
42	logger.LogWithTimestamp(msg)
43}
44
45func main() {
46	console := &ConsoleLogger{}
47	timed := &TimedConsoleLogger{}
48
49	logMessage(console, "Simple log message")
50	logMessage(timed, "Timed logger used as simple logger")
51
52	logImportantMessage(timed, "Important timestamped message")
53}

Key Principle: Depend on the smallest interface that provides what you need. This makes your code more flexible and easier to test.

Pattern 2: Accept Interfaces, Return Concrete Types

This is one of Go's most important design principles:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Notifier interface for dependency injection
 7type Notifier interface {
 8	Notify(message string) error
 9}
10
11// EmailNotifier is a concrete type
12type EmailNotifier struct {
13	smtpServer string
14}
15
16func (e *EmailNotifier) Notify(message string) error {
17	fmt.Printf("Sending email via %s: %s\n", e.smtpServer, message)
18	return nil
19}
20
21func (e *EmailNotifier) GetSMTPServer() string {
22	return e.smtpServer
23}
24
25// SMSNotifier is a concrete type
26type SMSNotifier struct {
27	gateway string
28}
29
30func (s *SMSNotifier) Notify(message string) error {
31	fmt.Printf("Sending SMS via %s: %s\n", s.gateway, message)
32	return nil
33}
34
35// Accept interface - flexible input
36func SendNotification(notifier Notifier, message string) error {
37	return notifier.Notify(message)
38}
39
40// Return concrete type - exposes all methods
41func NewEmailNotifier(server string) *EmailNotifier {
42	return &EmailNotifier{smtpServer: server}
43}
44
45func main() {
46	// Return concrete type gives full access
47	emailer := NewEmailNotifier("smtp.example.com")
48	fmt.Printf("SMTP Server: %s\n", emailer.GetSMTPServer())
49
50	// Accept interface allows flexibility
51	SendNotification(emailer, "Hello via Email")
52
53	sms := &SMSNotifier{gateway: "twilio.com"}
54	SendNotification(sms, "Hello via SMS")
55}

Why This Matters:

  • Accept interfaces: Callers can pass any implementation
  • Return concrete types: Callers have access to all methods
  • Maximum flexibility: Change implementations without changing callers

Pattern 3: Interface Segregation

Don't force implementations to provide methods they don't need:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Bad: Large interface with many responsibilities
 7type BadStorage interface {
 8	Read(key string) (string, error)
 9	Write(key, value string) error
10	Delete(key string) error
11	List() ([]string, error)
12	Backup() error
13	Restore() error
14}
15
16// Good: Segregated interfaces
17type Reader interface {
18	Read(key string) (string, error)
19}
20
21type Writer interface {
22	Write(key, value string) error
23}
24
25type Deleter interface {
26	Delete(key string) error
27}
28
29// ReadWriter combines multiple interfaces
30type ReadWriter interface {
31	Reader
32	Writer
33}
34
35// Simple in-memory storage
36type MemoryStorage struct {
37	data map[string]string
38}
39
40func NewMemoryStorage() *MemoryStorage {
41	return &MemoryStorage{data: make(map[string]string)}
42}
43
44func (m *MemoryStorage) Read(key string) (string, error) {
45	value, exists := m.data[key]
46	if !exists {
47		return "", fmt.Errorf("key not found: %s", key)
48	}
49	return value, nil
50}
51
52func (m *MemoryStorage) Write(key, value string) error {
53	m.data[key] = value
54	return nil
55}
56
57func (m *MemoryStorage) Delete(key string) error {
58	delete(m.data, key)
59	return nil
60}
61
62// Function only needs Reader interface
63func displayValue(r Reader, key string) {
64	value, err := r.Read(key)
65	if err != nil {
66		fmt.Printf("Error reading %s: %v\n", key, err)
67		return
68	}
69	fmt.Printf("%s = %s\n", key, value)
70}
71
72// Function only needs Writer interface
73func storeValue(w Writer, key, value string) {
74	if err := w.Write(key, value); err != nil {
75		fmt.Printf("Error writing: %v\n", err)
76	}
77}
78
79func main() {
80	storage := NewMemoryStorage()
81
82	// Can use storage with functions requiring different interfaces
83	storeValue(storage, "name", "Alice")
84	storeValue(storage, "city", "New York")
85
86	displayValue(storage, "name")
87	displayValue(storage, "city")
88}

Benefits:

  • Functions depend on minimal interfaces
  • Easy to test with focused mocks
  • Clear separation of concerns
  • Implementation can choose which interfaces to satisfy

Common Patterns and Pitfalls

Pattern 1: The Empty Interface

The empty interface accepts any type because it requires zero methods:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Function that accepts any type
 7func printAnything(v interface{}) {
 8	fmt.Printf("Value: %v, Type: %T\n", v, v)
 9}
10
11func main() {
12	printAnything(42)
13	printAnything("hello")
14	printAnything([]int{1, 2, 3})
15	printAnything(map[string]int{"a": 1})
16}

When to use interface{}:

  • When you truly need to accept any type
  • For generic data structures
  • For unmarshaling JSON with unknown structure

When to avoid interface{}:

  • When you know the specific types you'll receive
  • For public APIs (prefer type safety)

Pattern 2: Type Assertions and Type Switches

Extract concrete values from interfaces safely:

 1// run
 2package main
 3
 4import "fmt"
 5
 6func processValue(v interface{}) {
 7	// Safe type assertion with "comma ok"
 8	if str, ok := v.(string); ok {
 9		fmt.Printf("String: %s (length: %d)\n", str, len(str))
10		return
11	}
12
13	if num, ok := v.(int); ok {
14		fmt.Printf("Number: %d (doubled: %d)\n", num, num*2)
15		return
16	}
17
18	// Type switch for multiple types
19	switch val := v.(type) {
20	case []int:
21		sum := 0
22		for _, n := range val {
23			sum += n
24		}
25		fmt.Printf("Int slice: %v (sum: %d)\n", val, sum)
26	case map[string]string:
27		fmt.Printf("Map: %v (size: %d)\n", val, len(val))
28	case nil:
29		fmt.Println("Value is nil")
30	default:
31		fmt.Printf("Unknown type: %T with value: %v\n", v, v)
32	}
33}
34
35func main() {
36	values := []interface{}{
37		"hello world",
38		42,
39		[]int{1, 2, 3, 4, 5},
40		map[string]string{"key1": "value1"},
41		nil,
42		3.14,
43	}
44
45	for _, value := range values {
46		processValue(value)
47		fmt.Println()
48	}
49}

Safety Rules:

  1. Always use "comma ok" for single type assertions: val, ok := i.(Type)
  2. Use type switches for handling multiple types
  3. Always handle the nil case
  4. Prefer specific interfaces over interface{} when possible

Pattern 3: Nil Interface Confusion

A common source of bugs - interfaces with nil concrete values aren't themselves nil:

 1// run
 2package main
 3
 4import "fmt"
 5
 6type MyInterface interface {
 7	DoSomething()
 8}
 9
10type MyType struct {
11	value string
12}
13
14func (m *MyType) DoSomething() {
15	if m == nil {
16		fmt.Println("Receiver is nil, but method can still be called!")
17		return
18	}
19	fmt.Printf("Value: %s\n", m.value)
20}
21
22func returnsNilPointer() *MyType {
23	return nil
24}
25
26func main() {
27	var i MyInterface = returnsNilPointer()
28
29	fmt.Printf("i == nil: %t\n", i == nil)        // false!
30	fmt.Printf("i value: %v\n", i)                // <nil>
31	fmt.Printf("i type: %T\n", i)                 // *main.MyType
32
33	// This works! The interface is not nil, even though it contains a nil pointer
34	i.DoSomething()
35
36	// Proper nil checking
37	if i == nil {
38		fmt.Println("Interface is nil")
39	} else {
40		fmt.Println("Interface is NOT nil (contains nil pointer)")
41	}
42}

What's happening:

  • i holds a *MyType that is nil
  • But i itself is not nil - it contains type information
  • This is why i == nil returns false

Solution: Check the concrete type or design methods to handle nil receivers

Pitfall 1: Interface Pollution

Don't create interfaces when you don't need them:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Bad: Unnecessary interface
 7// type Calculator interface {
 8//     Add(a, b int) int
 9// }
10
11// Better: Just use a function directly
12func Add(a, b int) int {
13	return a + b
14}
15
16func AddAndPrint(a, b int) {
17	result := Add(a, b)
18	fmt.Printf("%d + %d = %d\n", a, b, result)
19}
20
21func main() {
22	AddAndPrint(10, 20)
23	AddAndPrint(5, 7)
24}

Rule of thumb: Wait until you have at least 2 implementations before creating an interface.

Pitfall 2: Large Interfaces

Keep interfaces small and focused:

  1// run
  2package main
  3
  4import "fmt"
  5
  6// Bad: God interface with too many responsibilities
  7// type UserService interface {
  8//     CreateUser(name, email string) (*User, error)
  9//     GetUser(id int) (*User, error)
 10//     UpdateUser(id int, user User) error
 11//     DeleteUser(id int) error
 12//     SearchUsers(query string) ([]*User, error)
 13//     GetUsersByDepartment(dept string) ([]*User, error)
 14//     UpdateUserPassword(id int, password string) error
 15//     ... 20 more methods
 16// }
 17
 18// Good: Small, focused interfaces
 19type UserReader interface {
 20	GetUser(id int) (*User, error)
 21	SearchUsers(query string) ([]*User, error)
 22}
 23
 24type UserWriter interface {
 25	CreateUser(name, email string) (*User, error)
 26	UpdateUser(id int, user *User) error
 27	DeleteUser(id int) error
 28}
 29
 30type User struct {
 31	ID    int
 32	Name  string
 33	Email string
 34}
 35
 36// Simple implementations
 37type SimpleUserStore struct {
 38	users map[int]*User
 39	nextID int
 40}
 41
 42func NewSimpleUserStore() *SimpleUserStore {
 43	return &SimpleUserStore{
 44		users: make(map[int]*User),
 45		nextID: 1,
 46	}
 47}
 48
 49func (s *SimpleUserStore) GetUser(id int) (*User, error) {
 50	user, exists := s.users[id]
 51	if !exists {
 52		return nil, fmt.Errorf("user not found")
 53	}
 54	return user, nil
 55}
 56
 57func (s *SimpleUserStore) SearchUsers(query string) ([]*User, error) {
 58	var results []*User
 59	for _, user := range s.users {
 60		if user.Name == query || user.Email == query {
 61			results = append(results, user)
 62		}
 63	}
 64	return results, nil
 65}
 66
 67func (s *SimpleUserStore) CreateUser(name, email string) (*User, error) {
 68	user := &User{
 69		ID:    s.nextID,
 70		Name:  name,
 71		Email: email,
 72	}
 73	s.users[user.ID] = user
 74	s.nextID++
 75	return user, nil
 76}
 77
 78func (s *SimpleUserStore) UpdateUser(id int, user *User) error {
 79	if _, exists := s.users[id]; !exists {
 80		return fmt.Errorf("user not found")
 81	}
 82	s.users[id] = user
 83	return nil
 84}
 85
 86func (s *SimpleUserStore) DeleteUser(id int) error {
 87	delete(s.users, id)
 88	return nil
 89}
 90
 91func main() {
 92	store := NewSimpleUserStore()
 93
 94	// Use as UserWriter
 95	user, _ := store.CreateUser("Alice", "alice@example.com")
 96	fmt.Printf("Created: %+v\n", user)
 97
 98	// Use as UserReader
 99	found, _ := store.GetUser(user.ID)
100	fmt.Printf("Found: %+v\n", found)
101}

Benefits of small interfaces:

  • Easier to implement
  • Clearer purpose
  • Better testability
  • Easier to compose

Integration and Mastery - Building Real Applications

Master Example: Plugin System

Let's build a complete plugin system that demonstrates advanced interface usage:

  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"strings"
  7)
  8
  9// Plugin interface defines the contract for all plugins
 10type Plugin interface {
 11	Name() string
 12	Version() string
 13	Execute(input string) (string, error)
 14	Initialize(config map[string]interface{}) error
 15	Cleanup() error
 16}
 17
 18// PluginManager handles plugin lifecycle
 19type PluginManager struct {
 20	plugins map[string]Plugin
 21	config  map[string]map[string]interface{}
 22}
 23
 24func NewPluginManager() *PluginManager {
 25	return &PluginManager{
 26		plugins: make(map[string]Plugin),
 27		config:  make(map[string]map[string]interface{}),
 28	}
 29}
 30
 31func (pm *PluginManager) Register(plugin Plugin) error {
 32	name := plugin.Name()
 33
 34	if _, exists := pm.plugins[name]; exists {
 35		return fmt.Errorf("plugin %s already registered", name)
 36	}
 37
 38	pm.plugins[name] = plugin
 39	fmt.Printf("Registered plugin: %s v%s\n", name, plugin.Version())
 40	return nil
 41}
 42
 43func (pm *PluginManager) Configure(pluginName string, config map[string]interface{}) error {
 44	plugin, exists := pm.plugins[pluginName]
 45	if !exists {
 46		return fmt.Errorf("plugin %s not found", pluginName)
 47	}
 48
 49	pm.config[pluginName] = config
 50	return plugin.Initialize(config)
 51}
 52
 53func (pm *PluginManager) Execute(pluginName, input string) (string, error) {
 54	plugin, exists := pm.plugins[pluginName]
 55	if !exists {
 56		return "", fmt.Errorf("plugin %s not found", pluginName)
 57	}
 58
 59	return plugin.Execute(input)
 60}
 61
 62func (pm *PluginManager) List() []string {
 63	var names []string
 64	for name := range pm.plugins {
 65		names = append(names, name)
 66	}
 67	return names
 68}
 69
 70func (pm *PluginManager) Cleanup() {
 71	for name, plugin := range pm.plugins {
 72		if err := plugin.Cleanup(); err != nil {
 73			fmt.Printf("Error cleaning up %s: %v\n", name, err)
 74		}
 75	}
 76}
 77
 78// UpperCasePlugin converts text to uppercase
 79type UpperCasePlugin struct{}
 80
 81func (p *UpperCasePlugin) Name() string {
 82	return "uppercase"
 83}
 84
 85func (p *UpperCasePlugin) Version() string {
 86	return "1.0.0"
 87}
 88
 89func (p *UpperCasePlugin) Execute(input string) (string, error) {
 90	return strings.ToUpper(input), nil
 91}
 92
 93func (p *UpperCasePlugin) Initialize(config map[string]interface{}) error {
 94	fmt.Printf("UpperCasePlugin initialized with config: %v\n", config)
 95	return nil
 96}
 97
 98func (p *UpperCasePlugin) Cleanup() error {
 99	fmt.Println("UpperCasePlugin cleaned up")
100	return nil
101}
102
103// ReversePlugin reverses text
104type ReversePlugin struct{}
105
106func (p *ReversePlugin) Name() string {
107	return "reverse"
108}
109
110func (p *ReversePlugin) Version() string {
111	return "1.0.0"
112}
113
114func (p *ReversePlugin) Execute(input string) (string, error) {
115	runes := []rune(input)
116	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
117		runes[i], runes[j] = runes[j], runes[i]
118	}
119	return string(runes), nil
120}
121
122func (p *ReversePlugin) Initialize(config map[string]interface{}) error {
123	fmt.Printf("ReversePlugin initialized with config: %v\n", config)
124	return nil
125}
126
127func (p *ReversePlugin) Cleanup() error {
128	fmt.Println("ReversePlugin cleaned up")
129	return nil
130}
131
132// CountPlugin counts words
133type CountPlugin struct {
134	caseSensitive bool
135}
136
137func (p *CountPlugin) Name() string {
138	return "count"
139}
140
141func (p *CountPlugin) Version() string {
142	return "1.0.0"
143}
144
145func (p *CountPlugin) Execute(input string) (string, error) {
146	text := input
147	if !p.caseSensitive {
148		text = strings.ToLower(text)
149	}
150	words := strings.Fields(text)
151	return fmt.Sprintf("Word count: %d", len(words)), nil
152}
153
154func (p *CountPlugin) Initialize(config map[string]interface{}) error {
155	if caseSensitive, ok := config["case_sensitive"].(bool); ok {
156		p.caseSensitive = caseSensitive
157	}
158	fmt.Printf("CountPlugin initialized with case_sensitive=%t\n", p.caseSensitive)
159	return nil
160}
161
162func (p *CountPlugin) Cleanup() error {
163	fmt.Println("CountPlugin cleaned up")
164	return nil
165}
166
167func main() {
168	// Create plugin manager
169	manager := NewPluginManager()
170
171	// Register plugins
172	plugins := []Plugin{
173		&UpperCasePlugin{},
174		&ReversePlugin{},
175		&CountPlugin{},
176	}
177
178	for _, plugin := range plugins {
179		if err := manager.Register(plugin); err != nil {
180			fmt.Printf("Error registering plugin: %v\n", err)
181		}
182	}
183
184	fmt.Printf("\nRegistered plugins: %v\n\n", manager.List())
185
186	// Configure plugins
187	manager.Configure("uppercase", map[string]interface{}{
188		"debug": true,
189	})
190
191	manager.Configure("count", map[string]interface{}{
192		"case_sensitive": false,
193	})
194
195	// Test plugins
196	testText := "Hello World Go Interfaces"
197
198	fmt.Println("=== Plugin Execution ===")
199	for _, pluginName := range manager.List() {
200		result, err := manager.Execute(pluginName, testText)
201		if err != nil {
202			fmt.Printf("Error executing %s: %v\n", pluginName, err)
203		} else {
204			fmt.Printf("%s: %s\n", pluginName, result)
205		}
206	}
207
208	// Cleanup
209	fmt.Println("\n=== Cleanup ===")
210	manager.Cleanup()
211}

Key Concepts Demonstrated:

  • Interface Design: Well-defined contract with clear responsibilities
  • Plugin Registration: Dynamic registration and discovery
  • Configuration: Plugin-specific configuration through interface methods
  • Error Handling: Proper error propagation through interface calls
  • Lifecycle Management: Initialize, execute, cleanup pattern

Practice Exercises

Exercise 1: Basic Interface Implementation

Learning Objectives: Understand implicit satisfaction and basic polymorphism

Difficulty: Beginner

Real-World Context: Implementing different animals that can speak and move, demonstrating how interfaces enable polymorphic behavior

Task: Create an Animal interface with Speak() and Move() methods. Implement it for Dog, Cat, and Bird types. Create a function that works with any Animal.

Show Solution
 1// run
 2package main
 3
 4import "fmt"
 5
 6// Animal interface
 7type Animal interface {
 8	Speak() string
 9	Move() string
10}
11
12// Dog implements Animal
13type Dog struct {
14	Name  string
15	Breed string
16}
17
18func (d Dog) Speak() string {
19	return fmt.Sprintf("%s barks loudly!", d.Name)
20}
21
22func (d Dog) Move() string {
23	return fmt.Sprintf("%s runs on four legs", d.Name)
24}
25
26// Cat implements Animal
27type Cat struct {
28	Name  string
29	Color string
30}
31
32func (c Cat) Speak() string {
33	return fmt.Sprintf("%s meows softly", c.Name)
34}
35
36func (c Cat) Move() string {
37	return fmt.Sprintf("%s walks gracefully", c.Name)
38}
39
40// Bird implements Animal
41type Bird struct {
42	Name    string
43	Species string
44}
45
46func (b Bird) Speak() string {
47	return fmt.Sprintf("%s chirps melodiously", b.Name)
48}
49
50func (b Bird) Move() string {
51	return fmt.Sprintf("%s flies through the air", b.Name)
52}
53
54// Function works with any Animal
55func describeAnimal(a Animal) {
56	fmt.Printf("Sound: %s\n", a.Speak())
57	fmt.Printf("Movement: %s\n", a.Move())
58	fmt.Println()
59}
60
61func main() {
62	animals := []Animal{
63		Dog{Name: "Buddy", Breed: "Golden Retriever"},
64		Cat{Name: "Whiskers", Color: "orange"},
65		Bird{Name: "Tweety", Species: "canary"},
66	}
67
68	fmt.Println("=== Animal Descriptions ===")
69	for _, animal := range animals {
70		describeAnimal(animal)
71	}
72}

Exercise 2: Interface Composition

Learning Objectives: Build complex interfaces from simple ones and implement them in real scenarios

Difficulty: Intermediate

Real-World Context: Creating a file system abstraction that demonstrates how small interfaces can be composed into larger ones

Task: Create Reader, Writer, and Closer interfaces. Compose them into ReadWriter and ReadWriteCloser interfaces. Implement a File type that satisfies ReadWriteCloser.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"io"
  7)
  8
  9// Basic interfaces
 10type Reader interface {
 11	Read() (string, error)
 12}
 13
 14type Writer interface {
 15	Write(string) error
 16}
 17
 18type Closer interface {
 19	Close() error
 20}
 21
 22// Composed interfaces
 23type ReadWriter interface {
 24	Reader
 25	Writer
 26}
 27
 28type ReadCloser interface {
 29	Reader
 30	Closer
 31}
 32
 33type ReadWriteCloser interface {
 34	Reader
 35	Writer
 36	Closer
 37}
 38
 39// File implementation
 40type File struct {
 41	name    string
 42	content []byte
 43	cursor  int
 44	closed  bool
 45}
 46
 47func NewFile(name string) *File {
 48	return &File{
 49		name:    name,
 50		content: []byte{},
 51		cursor:  0,
 52		closed:  false,
 53	}
 54}
 55
 56func (f *File) Read() (string, error) {
 57	if f.closed {
 58		return "", fmt.Errorf("file is closed")
 59	}
 60
 61	if f.cursor >= len(f.content) {
 62		return "", io.EOF
 63	}
 64
 65	result := string(f.content[f.cursor:])
 66	f.cursor = len(f.content)
 67	return result, nil
 68}
 69
 70func (f *File) Write(data string) error {
 71	if f.closed {
 72		return fmt.Errorf("file is closed")
 73	}
 74
 75	f.content = append(f.content, []byte(data)...)
 76	return nil
 77}
 78
 79func (f *File) Close() error {
 80	if f.closed {
 81		return fmt.Errorf("file is already closed")
 82	}
 83
 84	f.closed = true
 85	fmt.Printf("File '%s' closed. Size: %d bytes\n", f.name, len(f.content))
 86	return nil
 87}
 88
 89// Process different types of files
 90func processReader(r Reader) {
 91	fmt.Println("Processing Reader:")
 92	data, err := r.Read()
 93	if err != nil && err != io.EOF {
 94		fmt.Printf("Error: %v\n", err)
 95		return
 96	}
 97	fmt.Printf("Read: %q\n", data)
 98}
 99
100func processWriter(w Writer, data []string) error {
101	fmt.Println("Processing Writer:")
102	for _, item := range data {
103		if err := w.Write(item); err != nil {
104			return err
105		}
106		fmt.Printf("Wrote: %q\n", item)
107	}
108	return nil
109}
110
111func processReadWriteCloser(rwc ReadWriteCloser, data []string) error {
112	fmt.Println("Processing ReadWriteCloser:")
113
114	// Write data
115	for _, item := range data {
116		if err := rwc.Write(item); err != nil {
117			return err
118		}
119	}
120
121	// Reset cursor and read back
122	if file, ok := rwc.(*File); ok {
123		file.cursor = 0
124	}
125
126	// Read all data
127	fmt.Println("Reading back data:")
128	content, err := rwc.Read()
129	if err != nil && err != io.EOF {
130		return err
131	}
132	fmt.Printf("  %q\n", content)
133
134	return rwc.Close()
135}
136
137func main() {
138	// Test basic file operations
139	file := NewFile("test.txt")
140
141	data := []string{"Hello", " ", "World", "!"}
142
143	fmt.Println("=== Testing Basic Operations ===")
144
145	// Test as Writer
146	err := processWriter(file, data)
147	if err != nil {
148		fmt.Printf("Writer error: %v\n", err)
149	}
150
151	// Reset for Reader test
152	file.cursor = 0
153
154	// Test as Reader
155	processReader(file)
156
157	// Test as ReadWriteCloser
158	file2 := NewFile("test2.txt")
159	processReadWriteCloser(file2, []string{"Testing", " ", "ReadWriteCloser"})
160}

Exercise 3: Dependency Injection Pattern

Learning Objectives: Implement a complete dependency injection system using interfaces for a notification service

Difficulty: Advanced

Real-World Context: Building a multi-channel notification system that can send messages via email, SMS, or push notifications

Task: Create a Notification interface. Implement Email, SMS, and Push notifiers. Build a NotificationService that uses dependency injection to work with any notifier type.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"time"
  7)
  8
  9// Notification interface
 10type Notifier interface {
 11	Send(message string) error
 12	GetType() string
 13}
 14
 15// Message interface
 16type Message interface {
 17	GetContent() string
 18	GetPriority() int
 19	GetTimestamp() time.Time
 20}
 21
 22// Concrete message types
 23type EmailMessage struct {
 24	Content   string
 25	Priority  int
 26	Timestamp time.Time
 27	Sender    string
 28	Recipient string
 29}
 30
 31func (m EmailMessage) GetContent() string {
 32	return m.Content
 33}
 34
 35func (m EmailMessage) GetPriority() int {
 36	return m.Priority
 37}
 38
 39func (m EmailMessage) GetTimestamp() time.Time {
 40	return m.Timestamp
 41}
 42
 43type SMSMessage struct {
 44	Content     string
 45	Priority    int
 46	Timestamp   time.Time
 47	PhoneNumber string
 48}
 49
 50func (m SMSMessage) GetContent() string {
 51	return m.Content
 52}
 53
 54func (m SMSMessage) GetPriority() int {
 55	return m.Priority
 56}
 57
 58func (m SMSMessage) GetTimestamp() time.Time {
 59	return m.Timestamp
 60}
 61
 62// Concrete notification types
 63type EmailNotifier struct {
 64	smtpServer string
 65	from       string
 66}
 67
 68func (e *EmailNotifier) Send(message string) error {
 69	fmt.Printf("[EMAIL] From: %s, Message: %s\n", e.from, message)
 70	time.Sleep(50 * time.Millisecond)
 71	return nil
 72}
 73
 74func (e *EmailNotifier) GetType() string {
 75	return "email"
 76}
 77
 78type SMSNotifier struct {
 79	gateway  string
 80	apiToken string
 81}
 82
 83func (s *SMSNotifier) Send(message string) error {
 84	fmt.Printf("[SMS] Via: %s, Message: %s\n", s.gateway, message)
 85	time.Sleep(30 * time.Millisecond)
 86	return nil
 87}
 88
 89func (s *SMSNotifier) GetType() string {
 90	return "sms"
 91}
 92
 93type PushNotifier struct {
 94	service string
 95	apiKey  string
 96}
 97
 98func (p *PushNotifier) Send(message string) error {
 99	fmt.Printf("[PUSH] Service: %s, Message: %s\n", p.service, message)
100	time.Sleep(10 * time.Millisecond)
101	return nil
102}
103
104func (p *PushNotifier) GetType() string {
105	return "push"
106}
107
108// Notification service that depends on interfaces
109type NotificationService struct {
110	notifiers []Notifier
111}
112
113func NewNotificationService(notifiers ...Notifier) *NotificationService {
114	return &NotificationService{
115		notifiers: notifiers,
116	}
117}
118
119func (ns *NotificationService) AddNotifier(notifier Notifier) {
120	ns.notifiers = append(ns.notifiers, notifier)
121}
122
123func (ns *NotificationService) SendNotification(message Message) error {
124	fmt.Printf("Sending notification (priority %d): %s\n",
125		message.GetPriority(), message.GetContent())
126
127	errors := make(chan error, len(ns.notifiers))
128
129	// Send via all notifiers concurrently
130	for _, notifier := range ns.notifiers {
131		go func(n Notifier) {
132			err := n.Send(message.GetContent())
133			errors <- err
134		}(notifier)
135	}
136
137	// Collect results
138	var sendErrors []error
139	for i := 0; i < len(ns.notifiers); i++ {
140		if err := <-errors; err != nil {
141			sendErrors = append(sendErrors, err)
142		}
143	}
144
145	if len(sendErrors) > 0 {
146		return fmt.Errorf("some notifications failed: %v", sendErrors)
147	}
148
149	return nil
150}
151
152func (ns *NotificationService) SendHighPriority(message Message) error {
153	// Only send high priority notifications via email and SMS
154	var highPriorityNotifiers []Notifier
155	for _, notifier := range ns.notifiers {
156		notifierType := notifier.GetType()
157		if notifierType == "email" || notifierType == "sms" {
158			highPriorityNotifiers = append(highPriorityNotifiers, notifier)
159		}
160	}
161
162	if len(highPriorityNotifiers) == 0 {
163		return fmt.Errorf("no high priority notifiers available")
164	}
165
166	fmt.Println("Sending HIGH PRIORITY notification")
167	errors := make(chan error, len(highPriorityNotifiers))
168
169	for _, notifier := range highPriorityNotifiers {
170		go func(n Notifier) {
171			err := n.Send(message.GetContent())
172			errors <- err
173		}(notifier)
174	}
175
176	var sendErrors []error
177	for i := 0; i < len(highPriorityNotifiers); i++ {
178		if err := <-errors; err != nil {
179			sendErrors = append(sendErrors, err)
180		}
181	}
182
183	if len(sendErrors) > 0 {
184		return fmt.Errorf("some high priority notifications failed: %v", sendErrors)
185	}
186
187	return nil
188}
189
190// Mock notifier for testing
191type MockNotifier struct {
192	sentMessages []string
193	name         string
194}
195
196func NewMockNotifier(name string) *MockNotifier {
197	return &MockNotifier{
198		sentMessages: []string{},
199		name:         name,
200	}
201}
202
203func (m *MockNotifier) Send(message string) error {
204	m.sentMessages = append(m.sentMessages, message)
205	fmt.Printf("[MOCK-%s] Would send: %s\n", m.name, message)
206	return nil
207}
208
209func (m *MockNotifier) GetType() string {
210	return "mock-" + m.name
211}
212
213func (m *MockNotifier) GetSentMessages() []string {
214	return m.sentMessages
215}
216
217func main() {
218	fmt.Println("=== Production Notification Service ===")
219
220	// Production notifiers
221	emailNotifier := &EmailNotifier{
222		smtpServer: "smtp.example.com",
223		from:       "noreply@company.com",
224	}
225
226	smsNotifier := &SMSNotifier{
227		gateway:  "twilio.example.com",
228		apiToken: "secret-token",
229	}
230
231	pushNotifier := &PushNotifier{
232		service: "fcm.googleapis.com",
233		apiKey:  "fcm-secret",
234	}
235
236	// Create production service
237	prodService := NewNotificationService(emailNotifier, smsNotifier, pushNotifier)
238
239	// Test messages
240	emailMsg := EmailMessage{
241		Content:   "Welcome to our service!",
242		Priority:  1,
243		Timestamp: time.Now(),
244		Sender:    "system@company.com",
245		Recipient: "user@example.com",
246	}
247
248	smsMsg := SMSMessage{
249		Content:     "Your verification code is 123456",
250		Priority:    3,
251		Timestamp:   time.Now(),
252		PhoneNumber: "+1234567890",
253	}
254
255	// Send notifications
256	fmt.Println("Sending regular notifications:")
257	prodService.SendNotification(emailMsg)
258	time.Sleep(100 * time.Millisecond)
259	prodService.SendNotification(smsMsg)
260	time.Sleep(100 * time.Millisecond)
261
262	fmt.Println("\n=== Testing with Mock Notifiers ===")
263
264	// Mock notifiers for testing
265	mockEmail := NewMockNotifier("email")
266	mockSMS := NewMockNotifier("sms")
267	mockPush := NewMockNotifier("push")
268
269	testService := NewNotificationService(mockEmail, mockSMS, mockPush)
270
271	// High priority message
272	highPriorityMsg := EmailMessage{
273		Content:   "URGENT: System maintenance in 1 hour",
274		Priority:  5,
275		Timestamp: time.Now(),
276		Sender:    "alerts@company.com",
277		Recipient: "admin@company.com",
278	}
279
280	fmt.Println("Sending high priority notification:")
281	testService.SendHighPriority(highPriorityMsg)
282
283	time.Sleep(100 * time.Millisecond)
284
285	fmt.Println("\nMock notifications sent:")
286	fmt.Printf("Email mock: %v\n", mockEmail.GetSentMessages())
287	fmt.Printf("SMS mock: %v\n", mockSMS.GetSentMessages())
288	fmt.Printf("Push mock: %v\n", mockPush.GetSentMessages())
289}

Exercise 4: Mock Testing with Interfaces

Learning Objectives: Create a comprehensive testing framework using interfaces to demonstrate how interfaces enable reliable unit testing

Difficulty: Advanced

Real-World Context: Building a user service with database operations that can be easily tested without a real database

Task: Create a Database interface with CRUD operations. Implement both a production PostgreSQL version and a mock version for testing. Build a UserService that depends on the Database interface.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"strings"
  7	"time"
  8)
  9
 10// Database interface for testing
 11type Database interface {
 12	GetUser(id int) (*User, error)
 13	SaveUser(user *User) error
 14	DeleteUser(id int) error
 15	FindUsers(query string) ([]*User, error)
 16	Close() error
 17}
 18
 19// User model
 20type User struct {
 21	ID        int       `json:"id"`
 22	Name      string    `json:"name"`
 23	Email     string    `json:"email"`
 24	CreatedAt time.Time `json:"created_at"`
 25}
 26
 27// Production database implementation
 28type PostgreSQLDatabase struct {
 29	connectionString string
 30	users            map[int]*User
 31	nextID           int
 32}
 33
 34func NewPostgreSQLDatabase(connectionString string) *PostgreSQLDatabase {
 35	return &PostgreSQLDatabase{
 36		connectionString: connectionString,
 37		users:            make(map[int]*User),
 38		nextID:           1,
 39	}
 40}
 41
 42func (db *PostgreSQLDatabase) GetUser(id int) (*User, error) {
 43	user, exists := db.users[id]
 44	if !exists {
 45		return nil, fmt.Errorf("user with id %d not found", id)
 46	}
 47	return user, nil
 48}
 49
 50func (db *PostgreSQLDatabase) SaveUser(user *User) error {
 51	if user.ID == 0 {
 52		user.ID = db.nextID
 53		db.nextID++
 54		user.CreatedAt = time.Now()
 55	}
 56	db.users[user.ID] = user
 57	return nil
 58}
 59
 60func (db *PostgreSQLDatabase) DeleteUser(id int) error {
 61	if _, exists := db.users[id]; !exists {
 62		return fmt.Errorf("user with id %d not found", id)
 63	}
 64	delete(db.users, id)
 65	return nil
 66}
 67
 68func (db *PostgreSQLDatabase) FindUsers(query string) ([]*User, error) {
 69	var results []*User
 70	query = strings.ToLower(query)
 71
 72	for _, user := range db.users {
 73		if strings.Contains(strings.ToLower(user.Name), query) ||
 74			strings.Contains(strings.ToLower(user.Email), query) {
 75			results = append(results, user)
 76		}
 77	}
 78
 79	return results, nil
 80}
 81
 82func (db *PostgreSQLDatabase) Close() error {
 83	fmt.Println("PostgreSQL connection closed")
 84	return nil
 85}
 86
 87// Mock database for testing
 88type MockDatabase struct {
 89	users      map[int]*User
 90	getUserFn  func(id int) (*User, error)
 91	saveUserFn func(user *User) error
 92	delUserFn  func(id int) error
 93	findUsersFn func(query string) ([]*User, error)
 94	closeFn    func() error
 95}
 96
 97func NewMockDatabase() *MockDatabase {
 98	return &MockDatabase{
 99		users: make(map[int]*User),
100	}
101}
102
103func (m *MockDatabase) AddUser(user *User) {
104	if user.ID == 0 {
105		user.ID = len(m.users) + 1
106	}
107	m.users[user.ID] = user
108}
109
110func (m *MockDatabase) SetGetUserFunc(fn func(id int) (*User, error)) {
111	m.getUserFn = fn
112}
113
114func (m *MockDatabase) SetSaveUserFunc(fn func(user *User) error) {
115	m.saveUserFn = fn
116}
117
118func (m *MockDatabase) SetDeleteUserFunc(fn func(id int) error) {
119	m.delUserFn = fn
120}
121
122func (m *MockDatabase) SetFindUsersFunc(fn func(query string) ([]*User, error)) {
123	m.findUsersFn = fn
124}
125
126func (m *MockDatabase) SetCloseFunc(fn func() error) {
127	m.closeFn = fn
128}
129
130func (m *MockDatabase) GetUser(id int) (*User, error) {
131	if m.getUserFn != nil {
132		return m.getUserFn(id)
133	}
134
135	user, exists := m.users[id]
136	if !exists {
137		return nil, fmt.Errorf("user with id %d not found", id)
138	}
139	return user, nil
140}
141
142func (m *MockDatabase) SaveUser(user *User) error {
143	if m.saveUserFn != nil {
144		return m.saveUserFn(user)
145	}
146
147	if user.ID == 0 {
148		user.ID = len(m.users) + 1
149		user.CreatedAt = time.Now()
150	}
151	m.users[user.ID] = user
152	return nil
153}
154
155func (m *MockDatabase) DeleteUser(id int) error {
156	if m.delUserFn != nil {
157		return m.delUserFn(id)
158	}
159
160	if _, exists := m.users[id]; !exists {
161		return fmt.Errorf("user with id %d not found", id)
162	}
163	delete(m.users, id)
164	return nil
165}
166
167func (m *MockDatabase) FindUsers(query string) ([]*User, error) {
168	if m.findUsersFn != nil {
169		return m.findUsersFn(query)
170	}
171
172	var results []*User
173	for _, user := range m.users {
174		if strings.Contains(strings.ToLower(user.Name), strings.ToLower(query)) ||
175			strings.Contains(strings.ToLower(user.Email), strings.ToLower(query)) {
176			results = append(results, user)
177		}
178	}
179	return results, nil
180}
181
182func (m *MockDatabase) Close() error {
183	if m.closeFn != nil {
184		return m.closeFn()
185	}
186	return nil
187}
188
189// UserService depends on Database interface
190type UserService struct {
191	db Database
192}
193
194func NewUserService(db Database) *UserService {
195	return &UserService{db: db}
196}
197
198func (s *UserService) CreateUser(name, email string) (*User, error) {
199	user := &User{
200		Name:  name,
201		Email: email,
202	}
203
204	if err := s.db.SaveUser(user); err != nil {
205		return nil, fmt.Errorf("failed to create user: %w", err)
206	}
207
208	return user, nil
209}
210
211func (s *UserService) GetUser(id int) (*User, error) {
212	return s.db.GetUser(id)
213}
214
215func (s *UserService) UpdateUser(id int, name, email string) (*User, error) {
216	user, err := s.db.GetUser(id)
217	if err != nil {
218		return nil, fmt.Errorf("failed to get user: %w", err)
219	}
220
221	user.Name = name
222	user.Email = email
223
224	if err := s.db.SaveUser(user); err != nil {
225		return nil, fmt.Errorf("failed to update user: %w", err)
226	}
227
228	return user, nil
229}
230
231func (s *UserService) DeleteUser(id int) error {
232	return s.db.DeleteUser(id)
233}
234
235func (s *UserService) SearchUsers(query string) ([]*User, error) {
236	return s.db.FindUsers(query)
237}
238
239// Testing functions
240func testGetUser(service *UserService) error {
241	user, err := service.GetUser(1)
242	if err != nil {
243		return fmt.Errorf("GetUser failed: %w", err)
244	}
245	if user.Name != "Test User 1" {
246		return fmt.Errorf("expected 'Test User 1', got '%s'", user.Name)
247	}
248	fmt.Println("✓ testGetUser passed")
249	return nil
250}
251
252func testCreateUser(service *UserService) error {
253	user, err := service.CreateUser("New User", "new@example.com")
254	if err != nil {
255		return fmt.Errorf("CreateUser failed: %w", err)
256	}
257	if user.ID == 0 {
258		return fmt.Errorf("user ID should not be 0")
259	}
260	fmt.Println("✓ testCreateUser passed")
261	return nil
262}
263
264func testSearchUsers(service *UserService) error {
265	users, err := service.SearchUsers("Test")
266	if err != nil {
267		return fmt.Errorf("SearchUsers failed: %w", err)
268	}
269	if len(users) == 0 {
270		return fmt.Errorf("expected to find users with 'Test'")
271	}
272	fmt.Printf("✓ testSearchUsers passed (found %d users)\n", len(users))
273	return nil
274}
275
276func main() {
277	fmt.Println("=== Production Database Test ===")
278
279	// Test with real database
280	prodDB := NewPostgreSQLDatabase("postgres://localhost:5432/db")
281	prodService := NewUserService(prodDB)
282
283	// Create some users
284	user1, _ := prodService.CreateUser("Alice", "alice@example.com")
285	user2, _ := prodService.CreateUser("Bob", "bob@example.com")
286
287	fmt.Printf("Created users: %+v, %+v\n", user1, user2)
288
289	// Search functionality
290	results, _ := prodService.SearchUsers("Alice")
291	fmt.Printf("Search results: %+v\n", results)
292
293	prodDB.Close()
294
295	fmt.Println("\n=== Mock Database Testing ===")
296
297	// Test with mock database
298	mockDB := NewMockDatabase()
299
300	// Set up test data
301	mockDB.AddUser(&User{ID: 1, Name: "Test User 1", Email: "test1@example.com"})
302	mockDB.AddUser(&User{ID: 2, Name: "Test User 2", Email: "test2@example.com"})
303
304	testService := NewUserService(mockDB)
305
306	// Run tests
307	tests := []func(*UserService) error{
308		testGetUser,
309		testCreateUser,
310		testSearchUsers,
311	}
312
313	failedTests := 0
314	for i, test := range tests {
315		if err := test(testService); err != nil {
316			fmt.Printf("✗ Test %d failed: %v\n", i+1, err)
317			failedTests++
318		}
319	}
320
321	fmt.Printf("\n=== Test Summary ===\n")
322	fmt.Printf("Tests run: %d\n", len(tests))
323	fmt.Printf("Tests passed: %d\n", len(tests)-failedTests)
324	fmt.Printf("Tests failed: %d\n", failedTests)
325
326	if failedTests == 0 {
327		fmt.Println("All tests passed!")
328	}
329}

Exercise 5: Interface-Based Strategy Pattern

Learning Objectives: Implement the Strategy pattern using interfaces to enable runtime algorithm selection

Difficulty: Advanced

Real-World Context: Building a pricing calculator that can use different pricing strategies (regular, discount, premium) without changing the client code

Task: Create a PricingStrategy interface. Implement multiple pricing strategies (Regular, Discount, Premium). Build a Product and ShoppingCart that use strategy pattern for flexible pricing.

Show Solution
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"time"
  7)
  8
  9// PricingStrategy defines how to calculate prices
 10type PricingStrategy interface {
 11	CalculatePrice(basePrice float64, quantity int) float64
 12	GetName() string
 13	GetDescription() string
 14}
 15
 16// RegularPricing - no discounts
 17type RegularPricing struct{}
 18
 19func (p *RegularPricing) CalculatePrice(basePrice float64, quantity int) float64 {
 20	return basePrice * float64(quantity)
 21}
 22
 23func (p *RegularPricing) GetName() string {
 24	return "Regular Pricing"
 25}
 26
 27func (p *RegularPricing) GetDescription() string {
 28	return "Standard pricing with no discounts"
 29}
 30
 31// DiscountPricing - percentage discount
 32type DiscountPricing struct {
 33	discountPercent float64
 34}
 35
 36func NewDiscountPricing(percent float64) *DiscountPricing {
 37	return &DiscountPricing{discountPercent: percent}
 38}
 39
 40func (p *DiscountPricing) CalculatePrice(basePrice float64, quantity int) float64 {
 41	total := basePrice * float64(quantity)
 42	discount := total * (p.discountPercent / 100.0)
 43	return total - discount
 44}
 45
 46func (p *DiscountPricing) GetName() string {
 47	return fmt.Sprintf("Discount Pricing (%.0f%% off)", p.discountPercent)
 48}
 49
 50func (p *DiscountPricing) GetDescription() string {
 51	return fmt.Sprintf("%.0f%% discount on total price", p.discountPercent)
 52}
 53
 54// BulkPricing - tiered pricing based on quantity
 55type BulkPricing struct {
 56	tiers map[int]float64 // quantity threshold -> discount percent
 57}
 58
 59func NewBulkPricing() *BulkPricing {
 60	return &BulkPricing{
 61		tiers: map[int]float64{
 62			10: 5.0,  // 5% off for 10+ items
 63			20: 10.0, // 10% off for 20+ items
 64			50: 15.0, // 15% off for 50+ items
 65		},
 66	}
 67}
 68
 69func (p *BulkPricing) CalculatePrice(basePrice float64, quantity int) float64 {
 70	total := basePrice * float64(quantity)
 71
 72	// Find applicable discount tier
 73	discount := 0.0
 74	for threshold, percent := range p.tiers {
 75		if quantity >= threshold && percent > discount {
 76			discount = percent
 77		}
 78	}
 79
 80	return total * (1.0 - discount/100.0)
 81}
 82
 83func (p *BulkPricing) GetName() string {
 84	return "Bulk Pricing"
 85}
 86
 87func (p *BulkPricing) GetDescription() string {
 88	return "Tiered discounts: 5% off 10+, 10% off 20+, 15% off 50+"
 89}
 90
 91// PremiumPricing - time-based premium pricing
 92type PremiumPricing struct {
 93	premiumMultiplier float64
 94}
 95
 96func NewPremiumPricing(multiplier float64) *PremiumPricing {
 97	return &PremiumPricing{premiumMultiplier: multiplier}
 98}
 99
100func (p *PremiumPricing) CalculatePrice(basePrice float64, quantity int) float64 {
101	return basePrice * float64(quantity) * p.premiumMultiplier
102}
103
104func (p *PremiumPricing) GetName() string {
105	return fmt.Sprintf("Premium Pricing (%.1fx)", p.premiumMultiplier)
106}
107
108func (p *PremiumPricing) GetDescription() string {
109	return fmt.Sprintf("Premium service with %.1fx price multiplier", p.premiumMultiplier)
110}
111
112// Product represents an item for sale
113type Product struct {
114	ID          int
115	Name        string
116	BasePrice   float64
117	Description string
118}
119
120// CartItem represents a product in the cart
121type CartItem struct {
122	Product  *Product
123	Quantity int
124}
125
126// ShoppingCart uses strategy pattern for pricing
127type ShoppingCart struct {
128	items    []*CartItem
129	strategy PricingStrategy
130}
131
132func NewShoppingCart(strategy PricingStrategy) *ShoppingCart {
133	return &ShoppingCart{
134		items:    make([]*CartItem, 0),
135		strategy: strategy,
136	}
137}
138
139func (c *ShoppingCart) AddItem(product *Product, quantity int) {
140	c.items = append(c.items, &CartItem{
141		Product:  product,
142		Quantity: quantity,
143	})
144}
145
146func (c *ShoppingCart) SetPricingStrategy(strategy PricingStrategy) {
147	c.strategy = strategy
148}
149
150func (c *ShoppingCart) CalculateTotal() float64 {
151	total := 0.0
152	for _, item := range c.items {
153		itemTotal := c.strategy.CalculatePrice(item.Product.BasePrice, item.Quantity)
154		total += itemTotal
155	}
156	return total
157}
158
159func (c *ShoppingCart) PrintReceipt() {
160	fmt.Println("=" + strings.Repeat("=", 70))
161	fmt.Printf("SHOPPING CART RECEIPT\n")
162	fmt.Printf("Pricing Strategy: %s\n", c.strategy.GetName())
163	fmt.Printf("Description: %s\n", c.strategy.GetDescription())
164	fmt.Println("=" + strings.Repeat("=", 70))
165	fmt.Printf("%-30s %8s %10s %12s\n", "Product", "Qty", "Base", "Total")
166	fmt.Println("-" + strings.Repeat("-", 70))
167
168	for _, item := range c.items {
169		itemTotal := c.strategy.CalculatePrice(item.Product.BasePrice, item.Quantity)
170		fmt.Printf("%-30s %8d $%9.2f $%11.2f\n",
171			item.Product.Name,
172			item.Quantity,
173			item.Product.BasePrice,
174			itemTotal,
175		)
176	}
177
178	fmt.Println("-" + strings.Repeat("-", 70))
179	fmt.Printf("%-30s %8s %10s $%11.2f\n", "TOTAL", "", "", c.CalculateTotal())
180	fmt.Println("=" + strings.Repeat("=", 70))
181}
182
183func main() {
184	// Create products
185	products := []*Product{
186		{ID: 1, Name: "Laptop", BasePrice: 1000.00, Description: "High-performance laptop"},
187		{ID: 2, Name: "Mouse", BasePrice: 25.00, Description: "Wireless mouse"},
188		{ID: 3, Name: "Keyboard", BasePrice: 75.00, Description: "Mechanical keyboard"},
189	}
190
191	fmt.Println("=== STRATEGY PATTERN DEMO ===\n")
192
193	// Test 1: Regular Pricing
194	fmt.Println("TEST 1: Regular Pricing Strategy")
195	cart1 := NewShoppingCart(&RegularPricing{})
196	cart1.AddItem(products[0], 1) // 1 laptop
197	cart1.AddItem(products[1], 2) // 2 mice
198	cart1.PrintReceipt()
199	fmt.Println()
200
201	// Test 2: Discount Pricing (20% off)
202	fmt.Println("TEST 2: Discount Pricing Strategy (20% off)")
203	cart2 := NewShoppingCart(NewDiscountPricing(20))
204	cart2.AddItem(products[0], 1) // 1 laptop
205	cart2.AddItem(products[1], 2) // 2 mice
206	cart2.PrintReceipt()
207	fmt.Println()
208
209	// Test 3: Bulk Pricing
210	fmt.Println("TEST 3: Bulk Pricing Strategy")
211	cart3 := NewShoppingCart(NewBulkPricing())
212	cart3.AddItem(products[1], 25) // 25 mice (10% discount)
213	cart3.PrintReceipt()
214	fmt.Println()
215
216	// Test 4: Premium Pricing
217	fmt.Println("TEST 4: Premium Pricing Strategy (1.5x)")
218	cart4 := NewShoppingCart(NewPremiumPricing(1.5))
219	cart4.AddItem(products[0], 1) // 1 laptop
220	cart4.AddItem(products[2], 1) // 1 keyboard
221	cart4.PrintReceipt()
222	fmt.Println()
223
224	// Test 5: Dynamic Strategy Switching
225	fmt.Println("TEST 5: Dynamic Strategy Switching")
226	cart5 := NewShoppingCart(&RegularPricing{})
227	cart5.AddItem(products[0], 2) // 2 laptops
228
229	fmt.Println("Original pricing:")
230	originalTotal := cart5.CalculateTotal()
231	fmt.Printf("Total: $%.2f\n\n", originalTotal)
232
233	// Switch to discount pricing
234	cart5.SetPricingStrategy(NewDiscountPricing(15))
235	fmt.Println("After applying 15% discount:")
236	discountTotal := cart5.CalculateTotal()
237	fmt.Printf("Total: $%.2f\n", discountTotal)
238	fmt.Printf("Savings: $%.2f\n\n", originalTotal-discountTotal)
239
240	// Test 6: Comparing strategies
241	fmt.Println("TEST 6: Strategy Comparison for Bulk Purchase")
242	testProduct := products[1] // Mouse
243	testQuantity := 30
244
245	strategies := []PricingStrategy{
246		&RegularPricing{},
247		NewDiscountPricing(10),
248		NewBulkPricing(),
249		NewPremiumPricing(1.2),
250	}
251
252	fmt.Printf("Comparing prices for %d x %s ($%.2f each):\n\n",
253		testQuantity, testProduct.Name, testProduct.BasePrice)
254
255	for _, strategy := range strategies {
256		price := strategy.CalculatePrice(testProduct.BasePrice, testQuantity)
257		fmt.Printf("%-40s $%10.2f\n", strategy.GetName()+":", price)
258	}
259}

Summary

Key Takeaways

Mastered Core Concepts:

  • Implicit Satisfaction: Understand how types automatically satisfy interfaces
  • Interface Values: Know what's stored in interface values (type + value)
  • Dynamic Dispatch: Understand how method calls work with interfaces
  • Interface Design: Create small, focused interfaces
  • Composition: Build complex interfaces from simple ones
  • Polymorphism: Write flexible functions that work with multiple types

Real-World Benefits:

  • Testability: Easily mock dependencies for reliable unit tests
  • Flexibility: Swap implementations without changing consuming code
  • Extensibility: Add new functionality through new implementations
  • Team Collaboration: Different teams work independently on different implementations
  • Future-Proofing: Code adapts to new requirements without breaking

Critical Safety Rules:

  1. Prefer small interfaces - 1-3 methods is ideal
  2. Accept interfaces, return concrete types - maximum flexibility
  3. Wait for duplication - Don't create interfaces until you have 2+ implementations
  4. Handle nil interfaces carefully - they can contain nil concrete values
  5. Use type assertions safely - always check with "comma ok"
  6. Document interface contracts - be clear about behavior expectations

Best Practices Checklist

Practice Implemented Description
Small interfaces Keep interfaces focused on single responsibilities
Implicit satisfaction Rely on Go's automatic interface satisfaction
Interface composition Build complex interfaces from simple ones
Dependency injection Depend on interfaces, not concrete types
Mock testing Use interfaces to create test doubles
Error handling Handle interface-specific error cases
Type safety Use type assertions and switches safely

Decision Framework

When to create interfaces:

  • ✅ You have 2+ implementations of the same behavior
  • ✅ You need to test code with mock dependencies
  • ✅ You want to hide implementation details
  • ✅ You're building a library/framework for others to use

When NOT to create interfaces:

  • ❌ You only have one implementation
  • ❌ The interface adds unnecessary complexity
  • ❌ You're exposing internal implementation details
  • ❌ The interface is too large and unfocused

Interface Design Principles

  1. Define behavior, not implementation
  2. Keep interfaces small and focused
  3. Design for the consumer, not the implementer
  4. Use composition to build complex interfaces
  5. Document all behavior expectations
  6. Consider future extensibility

Next Steps in Your Go Journey

Now that you've mastered interfaces, you're ready for:

  1. Go Packages: Learn how to organize code with Go's package system
  2. Go Modules: Understand dependency management
  3. Concurrency: Learn how interfaces work with goroutines and channels
  4. Generics: Understand how generics complement interfaces in Go 1.18+
  5. Testing: Master unit testing patterns with interface-based mocking
  6. Architecture: Design larger systems using interface-driven development

Remember: Interfaces are Go's way of achieving flexibility without the complexity of traditional inheritance. Master them, and you'll write code that's both powerful and maintainable.

Interfaces turn Go's simplicity into strength - they allow you to build robust systems that can adapt to change without breaking.