📄 Analogy
Need ten similar contracts? You don’t retype each from a blank page — you photocopy a filled-in template and change the name and a clause. Prototype is that photocopier: start from a configured object and tweak the copy.
The problem
Sometimes building an object from scratch is expensive or fiddly — lots of setup, computed fields, loaded resources. If you already have one configured the way you want, it’s faster and simpler to clone it and adjust the copy. The catch in Go: a naive copy shares the original’s slices, maps, and pointers.
Structure
classDiagram
class Cloner {
<<interface>>
+Clone() Cloner
}
class Config {
+Name string
+Tags []string
+Clone() Config
}
Cloner <|.. Config
Config ..> Config : returns a deep copyIdiomatic Go — a deep Clone
Go has no built-in deep copy. cp := *c copies value fields but shares slices and maps, so Clone must duplicate them. Edit and Run — notice the base stays untouched:
package main
import "fmt"
type Config struct {
Name string
Tags []string
Limits map[string]int
}
// Clone returns a DEEP copy: slices and maps are duplicated, not shared.
func (c *Config) Clone() *Config {
cp := *c // copies Name, but Tags/Limits headers still point at c's data
cp.Tags = append([]string(nil), c.Tags...) // fresh slice
cp.Limits = make(map[string]int, len(c.Limits)) // fresh map
for k, v := range c.Limits {
cp.Limits[k] = v
}
return &cp
}
func main() {
base := &Config{
Name: "base",
Tags: []string{"prod"},
Limits: map[string]int{"cpu": 2},
}
clone := base.Clone()
clone.Name = "service-a"
clone.Tags = append(clone.Tags, "team-x")
clone.Limits["cpu"] = 8
fmt.Printf("base: %+v\n", *base) // unchanged
fmt.Printf("clone: %+v\n", *clone) // diverged independently
}
⚠️ The shallow-copy trap
Skip the slice/map duplication and you get a bug that looks like spooky action at a distance:
cp := *c // shallow: cp.Tags and c.Tags share one backing array
cp.Tags[0] = "oops" // ...so this also mutates the original!A plain *c copy is only safe when every field is itself a value type (no slices, maps, pointers, channels).
The prototype registry
A classic companion to Prototype is a registry of ready-made exemplars you clone by key — a factory whose products are copies of configured instances. Each Spawn hands back an independent clone, so callers can tweak freely while the stored prototype stays pristine:
package main
import "fmt"
type Shape struct {
Kind string
Sides int
Tags []string
}
func (s *Shape) Clone() *Shape {
cp := *s
cp.Tags = append([]string(nil), s.Tags...) // deep-copy the slice
return &cp
}
// A registry of pre-configured prototypes to clone on demand.
var prototypes = map[string]*Shape{
"triangle": {Kind: "triangle", Sides: 3, Tags: []string{"polygon"}},
"square": {Kind: "square", Sides: 4, Tags: []string{"polygon"}},
}
func Spawn(name string) (*Shape, bool) {
p, ok := prototypes[name]
if !ok {
return nil, false
}
return p.Clone(), true
}
func main() {
a, _ := Spawn("square")
a.Tags = append(a.Tags, "custom") // tweak the clone
b, _ := Spawn("square") // a fresh clone of the pristine prototype
fmt.Printf("a: %+v\n", *a)
fmt.Printf("b: %+v\n", *b)
}
In the standard library
Go gives you the cloning primitives directly (Go 1.21+):
slices.Clone(s)— copy a slice’s elements into a new backing array.maps.Clone(m)— shallow-copy a map.bytes.Clone(b)— copy a byte slice.
For arbitrary deep copies, a gob/json marshal-then-unmarshal round-trip works but is slow and drops unexported fields and functions — explicit Clone is usually better.
Pitfalls
⚠️ Deep clones get deep
If Config held a slice of pointers, slices.Clone would copy the pointers, not what they point to — the clone would still share those nested objects. Deep cloning is recursive, and the deeper the graph, the more error-prone. When that pain shows up, consider making the value immutable instead, so sharing is safe and no clone is needed.
When to use it — and when not
✅ Reach for it when
- Constructing an object is expensive, and you already have a fully-configured one to copy.
- You want to snapshot a live object's state and branch from it.
- You keep a registry of pre-built prototypes to clone on demand.
⛔ Think twice when
- The object is cheap and simple to construct — just construct it.
- Deep cloning is error-prone for the type (lots of nested references); prefer immutable values.
Related patterns
Construct a complex object step by step, separating how it's built from its final representation.
creationalFactory MethodDefine an interface for creating an object, but let the implementation decide which concrete type to instantiate.
behavioralMementoCapture and externalize an object's internal state so it can be restored later — without violating its encapsulation.
Check your understanding
Score: 0 / 51. In Go, what does `cp := *c` do when the struct has a slice field?
Struct assignment copies field values, but a slice field is just a header (ptr, len, cap) — so both structs point at the same underlying array until you copy it explicitly.
2. How do you make Clone correct for slices and maps?
Reference-type fields must be duplicated by hand (append([]T(nil), s...), a fresh map with copied entries) or via slices.Clone/maps.Clone, or clones will share mutable state.
3. When is Prototype most worthwhile?
Cloning pays off when building from scratch is costly or when you want a copy of an already-set-up object to tweak independently.
4. What is a prototype registry?
Keep a map[string]*T of fully-set-up prototypes; callers ask for one by name and get a fresh Clone. It's a factory whose 'products' are clones of stored exemplars — handy for spawning game entities, document templates, or default configs.
5. Does slices.Clone(s) give you a deep copy when s is a []*Node?
slices.Clone duplicates the backing array one level deep. For a slice of pointers (or structs containing references), the elements are still shared — true deep copy means recursively cloning what each pointer references, which is why deep cloning gets error-prone fast.
Comments
Sign in with GitHub to join the discussion.