Polymorphism Patterns in Go
Polymorphism is a key concept in object-oriented programming that allows you to write flexible, extensible code. Go achieves polymorphism through interfaces rather than inheritance. This guide covers practical polymorphism patterns in Go.
Understanding Polymorphism in Go
Interface-Based Polymorphism
package main
import (
"fmt"
)
// Define an interface
type Shape interface {
Area() float64
Perimeter() float64
}
// Implement for Circle
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
// Implement for Rectangle
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Polymorphic function
func PrintShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
circle := Circle{Radius: 5}
rectangle := Rectangle{Width: 4, Height: 6}
PrintShapeInfo(circle)
PrintShapeInfo(rectangle)
}
Duck Typing
package main
import (
"fmt"
"io"
"os"
)
// Writer interface (from io package)
type Writer interface {
Write(p []byte) (n int, err error)
}
// Custom writer
type LogWriter struct {
prefix string
}
func (lw LogWriter) Write(p []byte) (n int, err error) {
fmt.Printf("[%s] %s", lw.prefix, string(p))
return len(p), nil
}
func WriteData(w Writer, data []byte) error {
_, err := w.Write(data)
return err
}
func main() {
// Works with any Writer
WriteData(os.Stdout, []byte("Hello\n"))
WriteData(LogWriter{prefix: "LOG"}, []byte("Message\n"))
}
Polymorphic Patterns
Strategy Pattern
package main
import (
"fmt"
"sort"
)
// Strategy interface
type SortStrategy interface {
Sort(data []int)
}
// Bubble sort strategy
type BubbleSort struct{}
func (bs BubbleSort) Sort(data []int) {
n := len(data)
for i := 0; i < n; i++ {
for j := 0; j < n-i-1; j++ {
if data[j] > data[j+1] {
data[j], data[j+1] = data[j+1], data[j]
}
}
}
}
// Quick sort strategy
type QuickSort struct{}
func (qs QuickSort) Sort(data []int) {
sort.Ints(data)
}
// Context using strategy
type Sorter struct {
strategy SortStrategy
}
func (s *Sorter) Sort(data []int) {
s.strategy.Sort(data)
}
func main() {
data1 := []int{5, 2, 8, 1, 9}
data2 := []int{5, 2, 8, 1, 9}
sorter := &Sorter{strategy: BubbleSort{}}
sorter.Sort(data1)
fmt.Println("Bubble sort:", data1)
sorter.strategy = QuickSort{}
sorter.Sort(data2)
fmt.Println("Quick sort:", data2)
}
Factory Pattern
package main
import (
"fmt"
)
// Product interface
type Database interface {
Connect() error
Query(sql string) ([]string, error)
Close() error
}
// MySQL implementation
type MySQL struct {
host string
}
func (m *MySQL) Connect() error {
fmt.Println("Connecting to MySQL")
return nil
}
func (m *MySQL) Query(sql string) ([]string, error) {
fmt.Printf("MySQL query: %s\n", sql)
return []string{"result1", "result2"}, nil
}
func (m *MySQL) Close() error {
fmt.Println("Closing MySQL connection")
return nil
}
// PostgreSQL implementation
type PostgreSQL struct {
host string
}
func (p *PostgreSQL) Connect() error {
fmt.Println("Connecting to PostgreSQL")
return nil
}
func (p *PostgreSQL) Query(sql string) ([]string, error) {
fmt.Printf("PostgreSQL query: %s\n", sql)
return []string{"result1", "result2"}, nil
}
func (p *PostgreSQL) Close() error {
fmt.Println("Closing PostgreSQL connection")
return nil
}
// Factory function
func NewDatabase(dbType string) Database {
switch dbType {
case "mysql":
return &MySQL{host: "localhost"}
case "postgres":
return &PostgreSQL{host: "localhost"}
default:
return nil
}
}
func main() {
db := NewDatabase("mysql")
db.Connect()
db.Query("SELECT * FROM users")
db.Close()
}
Observer Pattern
package main
import (
"fmt"
)
// Observer interface
type Observer interface {
Update(message string)
}
// Subject
type EventBus struct {
observers []Observer
}
func (eb *EventBus) Subscribe(obs Observer) {
eb.observers = append(eb.observers, obs)
}
func (eb *EventBus) Publish(message string) {
for _, obs := range eb.observers {
obs.Update(message)
}
}
// Concrete observers
type EmailNotifier struct {
email string
}
func (en EmailNotifier) Update(message string) {
fmt.Printf("Email to %s: %s\n", en.email, message)
}
type SMSNotifier struct {
phone string
}
func (sn SMSNotifier) Update(message string) {
fmt.Printf("SMS to %s: %s\n", sn.phone, message)
}
func main() {
bus := &EventBus{}
bus.Subscribe(EmailNotifier{email: "[email protected]"})
bus.Subscribe(SMSNotifier{phone: "555-1234"})
bus.Publish("System alert!")
}
Decorator Pattern
package main
import (
"fmt"
)
// Component interface
type Component interface {
Operation() string
}
// Concrete component
type SimpleComponent struct{}
func (sc SimpleComponent) Operation() string {
return "SimpleComponent"
}
// Decorator
type Decorator struct {
component Component
}
type UppercaseDecorator struct {
Decorator
}
func (ud UppercaseDecorator) Operation() string {
return fmt.Sprintf("UPPERCASE[%s]", ud.component.Operation())
}
type BracketDecorator struct {
Decorator
}
func (bd BracketDecorator) Operation() string {
return fmt.Sprintf("[%s]", bd.component.Operation())
}
func main() {
component := SimpleComponent{}
fmt.Println(component.Operation())
// Decorate with uppercase
upper := UppercaseDecorator{Decorator{component}}
fmt.Println(upper.Operation())
// Decorate with brackets
bracket := BracketDecorator{Decorator{upper}}
fmt.Println(bracket.Operation())
}
Practical Examples
Plugin System
package main
import (
"fmt"
)
// Plugin interface
type Plugin interface {
Name() string
Execute(input string) string
}
// Concrete plugins
type UpperPlugin struct{}
func (up UpperPlugin) Name() string {
return "Upper"
}
func (up UpperPlugin) Execute(input string) string {
return fmt.Sprintf("UPPER: %s", input)
}
type ReversePlugin struct{}
func (rp ReversePlugin) Name() string {
return "Reverse"
}
func (rp ReversePlugin) Execute(input string) string {
runes := []rune(input)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return fmt.Sprintf("REVERSE: %s", string(runes))
}
// Plugin manager
type PluginManager struct {
plugins map[string]Plugin
}
func NewPluginManager() *PluginManager {
return &PluginManager{
plugins: make(map[string]Plugin),
}
}
func (pm *PluginManager) Register(plugin Plugin) {
pm.plugins[plugin.Name()] = plugin
}
func (pm *PluginManager) Execute(name, input string) string {
if plugin, ok := pm.plugins[name]; ok {
return plugin.Execute(input)
}
return "Plugin not found"
}
func main() {
manager := NewPluginManager()
manager.Register(UpperPlugin{})
manager.Register(ReversePlugin{})
fmt.Println(manager.Execute("Upper", "hello"))
fmt.Println(manager.Execute("Reverse", "hello"))
}
Handler Chain
package main
import (
"fmt"
)
// Handler interface
type Handler interface {
Handle(request string) string
SetNext(handler Handler) Handler
}
// Base handler
type BaseHandler struct {
next Handler
}
func (bh *BaseHandler) SetNext(handler Handler) Handler {
bh.next = handler
return handler
}
func (bh *BaseHandler) PassToNext(request string) string {
if bh.next != nil {
return bh.next.Handle(request)
}
return "End of chain"
}
// Concrete handlers
type AuthHandler struct {
BaseHandler
}
func (ah *AuthHandler) Handle(request string) string {
fmt.Println("AuthHandler: Checking authentication")
return ah.PassToNext(request)
}
type ValidationHandler struct {
BaseHandler
}
func (vh *ValidationHandler) Handle(request string) string {
fmt.Println("ValidationHandler: Validating request")
return vh.PassToNext(request)
}
type ProcessHandler struct {
BaseHandler
}
func (ph *ProcessHandler) Handle(request string) string {
fmt.Println("ProcessHandler: Processing request")
return "Request processed"
}
func main() {
auth := &AuthHandler{}
validation := &ValidationHandler{}
process := &ProcessHandler{}
auth.SetNext(validation).SetNext(process)
result := auth.Handle("GET /api/users")
fmt.Println(result)
}
Best Practices
โ Good Practices
// Use interfaces for polymorphism
type Reader interface {
Read(p []byte) (n int, err error)
}
// Accept interfaces, return concrete types
func ProcessData(r Reader) ([]byte, error) {
// Process using interface
}
// Use type switches carefully
func Handle(v interface{}) {
switch v.(type) {
case string:
// Handle string
case int:
// Handle int
}
}
// Compose interfaces
type ReadWriter interface {
Reader
Writer
}
โ Anti-Patterns
// Don't use empty interface everywhere
func BadFunction(v interface{}) {
// Loses type safety
}
// Don't create unnecessary interfaces
type SingleMethodInterface interface {
OnlyOneMethod()
}
// Don't ignore type assertion errors
value := v.(string) // Panics if not string
// Don't create deep interface hierarchies
// Keep it simple and flat
Resources
Summary
Polymorphism in Go through interfaces enables:
- Flexible, extensible code
- Loose coupling between components
- Easy testing with mock implementations
- Clear contracts between packages
- Practical design patterns
Key patterns include Strategy, Factory, Observer, and Decorator. Use interfaces wisely to create maintainable, testable Go applications.
Comments