Prototype Pattern in Go: Clone Interface

The Prototype pattern creates new objects by cloning existing instances rather than constructing them from scratch. This approach shines when object creation is expensive, when you need...

Key Insights

  • The Prototype pattern in Go leverages interfaces to clone objects without coupling to their concrete types, making it ideal for expensive initialization or runtime-configured objects.
  • Deep cloning requires explicit handling of pointer fields, slices, and maps—Go’s value semantics only give you shallow copies by default.
  • A thread-safe prototype registry enables efficient object creation by cloning pre-configured templates rather than rebuilding from scratch.

Introduction to the Prototype Pattern

The Prototype pattern creates new objects by cloning existing instances rather than constructing them from scratch. This approach shines when object creation is expensive, when you need runtime-configured templates, or when you want to avoid a proliferation of factory subclasses.

Consider a game engine that spawns thousands of enemies per level. Each enemy type shares base stats, behaviors, and inventory configurations. Constructing each enemy from scratch wastes cycles. Instead, you create prototype instances once, then clone them as needed.

Compared to other creational patterns, Prototype differs fundamentally in its approach. Factory patterns delegate creation to specialized classes. Builder patterns construct complex objects step-by-step. Prototype sidesteps both by saying: “Here’s a working instance—copy it.”

Go’s lack of inheritance makes Prototype particularly attractive. Rather than building complex factory hierarchies, you define a simple interface and let each type handle its own cloning logic.

Designing the Clone Interface in Go

The foundation of Go’s Prototype implementation is a Cloneable interface. Keep it minimal:

type Cloneable interface {
    Clone() Cloneable
}

This interface returns the interface type itself, enabling polymorphic cloning. You can clone any Cloneable without knowing its concrete type.

However, you’ll often want type-safe cloning within a domain. Consider a more specific approach:

type Prototype[T any] interface {
    Clone() T
}

type Document interface {
    Prototype[Document]
    GetContent() string
    SetContent(string)
}

The generic version provides compile-time type safety while maintaining flexibility. Your choice depends on whether you need cross-type polymorphism or prefer stronger typing within a bounded context.

The critical design decision is deep versus shallow copying. Go’s assignment operator performs shallow copies—it copies pointer values, not the data they point to. For prototypes, you almost always want deep copies. Cloned objects must be independent; modifying a clone should never affect the original.

Implementing Shallow Clones

Let’s examine why shallow clones fail for most real-world cases:

type Author struct {
    Name  string
    Email string
}

type Document struct {
    Title    string
    Author   *Author
    Tags     []string
    Metadata map[string]string
}

func (d *Document) ShallowClone() *Document {
    copy := *d
    return &copy
}

This looks reasonable but creates dangerous shared state:

func main() {
    original := &Document{
        Title:    "Design Patterns",
        Author:   &Author{Name: "Alice", Email: "alice@example.com"},
        Tags:     []string{"go", "patterns"},
        Metadata: map[string]string{"version": "1.0"},
    }

    clone := original.ShallowClone()
    
    // These modifications affect BOTH objects
    clone.Author.Name = "Bob"
    clone.Tags[0] = "rust"
    clone.Metadata["version"] = "2.0"

    fmt.Println(original.Author.Name)      // "Bob" - corrupted!
    fmt.Println(original.Tags[0])          // "rust" - corrupted!
    fmt.Println(original.Metadata["version"]) // "2.0" - corrupted!
}

The shallow clone copies pointer addresses, not the underlying data. Both original and clone share the same Author, Tags slice backing array, and Metadata map. This defeats the entire purpose of cloning.

Implementing Deep Clones

Deep cloning requires explicit copying of all reference types. Here’s a proper implementation:

type ServerConfig struct {
    Host     string
    Port     int
    TLS      *TLSConfig
    Headers  map[string]string
    Backends []BackendConfig
}

type TLSConfig struct {
    CertPath string
    KeyPath  string
    Insecure bool
}

type BackendConfig struct {
    URL      string
    Weight   int
    Metadata map[string]string
}

func (c *ServerConfig) Clone() *ServerConfig {
    if c == nil {
        return nil
    }

    clone := &ServerConfig{
        Host: c.Host,
        Port: c.Port,
    }

    // Clone pointer field
    if c.TLS != nil {
        clone.TLS = &TLSConfig{
            CertPath: c.TLS.CertPath,
            KeyPath:  c.TLS.KeyPath,
            Insecure: c.TLS.Insecure,
        }
    }

    // Clone map
    if c.Headers != nil {
        clone.Headers = make(map[string]string, len(c.Headers))
        for k, v := range c.Headers {
            clone.Headers[k] = v
        }
    }

    // Clone slice with nested structures
    if c.Backends != nil {
        clone.Backends = make([]BackendConfig, len(c.Backends))
        for i, backend := range c.Backends {
            clone.Backends[i] = BackendConfig{
                URL:    backend.URL,
                Weight: backend.Weight,
            }
            if backend.Metadata != nil {
                clone.Backends[i].Metadata = make(map[string]string, len(backend.Metadata))
                for k, v := range backend.Metadata {
                    clone.Backends[i].Metadata[k] = v
                }
            }
        }
    }

    return clone
}

This implementation handles nil checks, copies maps key-by-key, and recursively clones nested structures. It’s verbose but explicit—you know exactly what gets copied.

For complex hierarchies, consider extracting clone methods for each nested type:

func (b *BackendConfig) Clone() BackendConfig {
    clone := BackendConfig{
        URL:    b.URL,
        Weight: b.Weight,
    }
    if b.Metadata != nil {
        clone.Metadata = make(map[string]string, len(b.Metadata))
        for k, v := range b.Metadata {
            clone.Metadata[k] = v
        }
    }
    return clone
}

Prototype Registry Pattern

A prototype registry stores named prototypes and dispenses clones on demand. This centralizes prototype management and enables runtime registration:

type Cloneable interface {
    Clone() Cloneable
}

type PrototypeRegistry struct {
    mu         sync.RWMutex
    prototypes map[string]Cloneable
}

func NewPrototypeRegistry() *PrototypeRegistry {
    return &PrototypeRegistry{
        prototypes: make(map[string]Cloneable),
    }
}

func (r *PrototypeRegistry) Register(name string, prototype Cloneable) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.prototypes[name] = prototype
}

func (r *PrototypeRegistry) Unregister(name string) {
    r.mu.Lock()
    defer r.mu.Unlock()
    delete(r.prototypes, name)
}

func (r *PrototypeRegistry) Clone(name string) (Cloneable, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    prototype, exists := r.prototypes[name]
    if !exists {
        return nil, fmt.Errorf("prototype %q not found", name)
    }
    
    return prototype.Clone(), nil
}

func (r *PrototypeRegistry) List() []string {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    names := make([]string, 0, len(r.prototypes))
    for name := range r.prototypes {
        names = append(names, name)
    }
    return names
}

The registry uses sync.RWMutex for thread safety—multiple goroutines can clone simultaneously, but registration requires exclusive access.

Real-World Application: Game Entity System

Let’s build a practical example: a game entity system that clones enemies with stats, inventory, and behaviors:

type Stats struct {
    Health    int
    Attack    int
    Defense   int
    Speed     float64
}

type Item struct {
    ID       string
    Name     string
    Quantity int
}

type Behavior struct {
    Type       string
    Parameters map[string]interface{}
}

type Enemy struct {
    ID        string
    Name      string
    Stats     Stats
    Inventory []Item
    Behaviors []*Behavior
    Position  *Vector3
}

type Vector3 struct {
    X, Y, Z float64
}

func (e *Enemy) Clone() Cloneable {
    clone := &Enemy{
        ID:    generateID(), // New unique ID for clone
        Name:  e.Name,
        Stats: e.Stats, // Value type, copied automatically
    }

    // Clone inventory
    if e.Inventory != nil {
        clone.Inventory = make([]Item, len(e.Inventory))
        copy(clone.Inventory, e.Inventory) // Items are value types
    }

    // Clone behaviors (pointer slice requires deep copy)
    if e.Behaviors != nil {
        clone.Behaviors = make([]*Behavior, len(e.Behaviors))
        for i, b := range e.Behaviors {
            clone.Behaviors[i] = b.Clone()
        }
    }

    // Clone position
    if e.Position != nil {
        clone.Position = &Vector3{
            X: e.Position.X,
            Y: e.Position.Y,
            Z: e.Position.Z,
        }
    }

    return clone
}

func (b *Behavior) Clone() *Behavior {
    clone := &Behavior{
        Type: b.Type,
    }
    if b.Parameters != nil {
        clone.Parameters = make(map[string]interface{}, len(b.Parameters))
        for k, v := range b.Parameters {
            clone.Parameters[k] = v // Note: nested references not handled
        }
    }
    return clone
}

func generateID() string {
    return fmt.Sprintf("enemy_%d", time.Now().UnixNano())
}

Usage in a game loop:

func main() {
    registry := NewPrototypeRegistry()

    // Register enemy prototypes
    goblin := &Enemy{
        Name:  "Goblin",
        Stats: Stats{Health: 50, Attack: 10, Defense: 5, Speed: 1.2},
        Inventory: []Item{
            {ID: "gold", Name: "Gold Coins", Quantity: 10},
        },
        Behaviors: []*Behavior{
            {Type: "patrol", Parameters: map[string]interface{}{"radius": 10.0}},
            {Type: "aggressive", Parameters: map[string]interface{}{"range": 5.0}},
        },
    }
    registry.Register("goblin", goblin)

    // Spawn enemies by cloning
    for i := 0; i < 100; i++ {
        cloned, _ := registry.Clone("goblin")
        enemy := cloned.(*Enemy)
        enemy.Position = &Vector3{X: float64(i * 10), Y: 0, Z: 0}
        // Add to game world...
    }
}

This approach dramatically outperforms constructor-based creation when prototypes require expensive initialization like loading assets, computing AI parameters, or parsing configuration files.

Testing and Best Practices

Clone implementations demand rigorous testing. Verify that modifications to clones don’t affect originals:

func TestEnemyClone_Independence(t *testing.T) {
    tests := []struct {
        name   string
        modify func(*Enemy)
        check  func(*Enemy, *Enemy) bool
    }{
        {
            name: "stats modification",
            modify: func(e *Enemy) {
                e.Stats.Health = 999
            },
            check: func(original, clone *Enemy) bool {
                return original.Stats.Health != clone.Stats.Health
            },
        },
        {
            name: "inventory modification",
            modify: func(e *Enemy) {
                e.Inventory[0].Quantity = 999
            },
            check: func(original, clone *Enemy) bool {
                return original.Inventory[0].Quantity != clone.Inventory[0].Quantity
            },
        },
        {
            name: "behavior parameter modification",
            modify: func(e *Enemy) {
                e.Behaviors[0].Parameters["radius"] = 999.0
            },
            check: func(original, clone *Enemy) bool {
                return original.Behaviors[0].Parameters["radius"] != clone.Behaviors[0].Parameters["radius"]
            },
        },
        {
            name: "position modification",
            modify: func(e *Enemy) {
                e.Position.X = 999
            },
            check: func(original, clone *Enemy) bool {
                return original.Position.X != clone.Position.X
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            original := createTestEnemy()
            clone := original.Clone().(*Enemy)

            tt.modify(clone)

            if !tt.check(original, clone) {
                t.Error("clone modification affected original")
            }
        })
    }
}

func createTestEnemy() *Enemy {
    return &Enemy{
        Name:  "Test",
        Stats: Stats{Health: 100},
        Inventory: []Item{
            {ID: "item1", Quantity: 5},
        },
        Behaviors: []*Behavior{
            {Type: "test", Parameters: map[string]interface{}{"radius": 10.0}},
        },
        Position: &Vector3{X: 0, Y: 0, Z: 0},
    }
}

Handle unexported fields by providing clone methods on types that own them. For circular references, use a visited map to track already-cloned objects. When dealing with interfaces in fields, ensure all concrete implementations satisfy Cloneable.

The Prototype pattern trades implementation complexity for runtime flexibility. Use it when object creation is genuinely expensive or when you need runtime-configurable templates. For simple structs with no reference types, direct construction remains simpler and clearer.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.