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:
- Interface Philosophy: Understand Go's revolutionary implicit satisfaction approach
- Interface Design: Create small, focused interfaces that are easy to implement
- Polymorphism: Write functions that work with multiple types seamlessly
- Best Practices: Apply "Accept interfaces, return structs" principle effectively
- Testing Patterns: Use interfaces to create testable, mock-friendly code
- 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:
Speakerinterface requiresSpeak() stringmethodDoghas that method → Dog automatically satisfiesSpeakerCathas that method → Cat automatically satisfiesSpeaker- No explicit declaration needed!
Interface Values: What's Under the Hood?
An interface value holds two things:
- Type information
- 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:
- Always use "comma ok" for single type assertions:
val, ok := i.(Type) - Use type switches for handling multiple types
- Always handle the
nilcase - 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:
iholds a*MyTypethat isnil- But
iitself is notnil- it contains type information - This is why
i == nilreturnsfalse
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:
- Prefer small interfaces - 1-3 methods is ideal
- Accept interfaces, return concrete types - maximum flexibility
- Wait for duplication - Don't create interfaces until you have 2+ implementations
- Handle nil interfaces carefully - they can contain nil concrete values
- Use type assertions safely - always check with "comma ok"
- 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
- Define behavior, not implementation
- Keep interfaces small and focused
- Design for the consumer, not the implementer
- Use composition to build complex interfaces
- Document all behavior expectations
- Consider future extensibility
Next Steps in Your Go Journey
Now that you've mastered interfaces, you're ready for:
- Go Packages: Learn how to organize code with Go's package system
- Go Modules: Understand dependency management
- Concurrency: Learn how interfaces work with goroutines and channels
- Generics: Understand how generics complement interfaces in Go 1.18+
- Testing: Master unit testing patterns with interface-based mocking
- 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.