deep is a comprehensive Go library for comparing, cloning, and synchronizing complex data structures. Deep introduces a revolutionary architecture centered on Code Generation and Type-Safe Selectors, delivering up to 15x performance improvements over traditional reflection-based libraries.
- Extreme Performance: Reflection-free operations via
deep-gen(10x-20x faster than v4). - Compile-Time Safety: Type-safe field selectors replace brittle string paths.
- Data-Oriented: Patches are pure, flat data structures, natively serializable to JSON.
- Integrated Causality: Native support for HLC (Hybrid Logical Clocks) and LWW (Last-Write-Wins).
- First-Class CRDTs: Built-in support for
TextandLWW[T]convergent registers. - Standard Compliant: Export to RFC 6902 JSON Patch with advanced predicate extensions.
- Hybrid Architecture: Optimized generated paths with a robust reflection safety net.
Benchmarks performed on typical struct models (User with IDs, Names, Slices):
| Operation | v4 (Reflection) | Deep (Generated) | Speedup |
|---|---|---|---|
| Apply Patch | 726 ns/op | 50 ns/op | 14.5x |
| Diff + Apply | 2,391 ns/op | 270 ns/op | 8.8x |
| Clone | 1,872 ns/op | 290 ns/op | 6.4x |
| Equality | 202 ns/op | 84 ns/op | 2.4x |
Run go test -bench=. ./... to reproduce. BenchmarkApplyGenerated uses generated code;
BenchmarkApplyReflection uses the fallback path on a type with no generated code.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Roles []string `json:"roles"`
Score map[string]int `json:"score"`
}Add a go:generate directive to your source file:
//go:generate go run github.com/brunoga/deep/v5/cmd/deep-gen -type=User .Then run:
go generate ./...This writes user_deep.go in the same directory. Commit it alongside your source.
import deep "github.com/brunoga/deep/v5"
u1 := User{ID: 1, Name: "Alice", Roles: []string{"user"}}
u2 := User{ID: 1, Name: "Bob", Roles: []string{"user", "admin"}}
// State-based Diffing
patch, err := deep.Diff(u1, u2)
if err != nil {
log.Fatal(err)
}
// Operation-based Building (Fluent, Type-Safe API)
namePath := deep.Field(func(u *User) *string { return &u.Name })
scorePath := deep.Field(func(u *User) *map[string]int { return &u.Score })
patch2 := deep.Edit(&u1).
With(
deep.Set(namePath, "Alice Smith"),
deep.Add(deep.MapKey(scorePath, "power"), 100),
).
Build()
// Application
if err := deep.Apply(&u1, patch); err != nil {
log.Fatal(err)
}Convert any field into a convergent register:
type Document struct {
Title crdt.LWW[string] // Native Last-Write-Wins
Content crdt.Text // Collaborative Text CRDT
}Conditions travel with the patch and are enforced wherever it is applied — including on remote peers via JSON Patch interop.
Global guard — the entire patch is a no-op if the condition fails. Use this for state-machine transitions where partial application would leave things in a bad state:
statusPath := deep.Field(func(o *Order) *string { return &o.Status })
// Atomically transition to "shipped" only if the order is currently "paid".
patch := deep.Edit(&order).
With(deep.Set(statusPath, "shipped")).
Guard(deep.Eq(statusPath, "paid")).
Build()Per-operation conditions (Op.If / Op.Unless) — individual operations are
skipped independently within a single patch. Use this when some fields are
conditional but others should always be updated:
paidAtPath := deep.Field(func(i *Invoice) *time.Time { return &i.PaidAt })
feePath := deep.Field(func(i *Invoice) *float64 { return &i.LateFee })
balancePath := deep.Field(func(i *Invoice) *float64 { return &i.Balance })
// Always record the payment time; only add a late fee if a balance is overdue.
patch := deep.Edit(&invoice).
With(deep.Set(paidAtPath, time.Now())).
With(deep.Set(feePath, 25.0).If(deep.Gt(balancePath, 0.0))).
Build()Embed OpLog operations in a patch to emit structured trace messages during Apply.
Route them to any *slog.Logger — useful for request-scoped loggers, test capture, or
tracing without touching your model types:
namePath := deep.Field(func(u *User) *string { return &u.Name })
patch := deep.Edit(&u).
Log("starting update").
With(deep.Set(namePath, "Alice Smith")).
Log("update complete").
Build()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
deep.Apply(&u, patch, deep.WithLogger(logger))
// {"level":"INFO","msg":"deep log","message":"starting update","path":"/"}
// {"level":"INFO","msg":"deep log","message":"update complete","path":"/"}When no logger is provided, slog.Default() is used — so existing slog.SetDefault
configuration is respected without any extra wiring.
// Reverse a patch to produce an undo patch.
undo := patch.Reverse()
// Enable strict mode — Apply verifies Old values match before each operation.
strictPatch := patch.AsStrict()CRDT[T].Reverse applies the inverse of a delta to the local node and returns a
new Delta[T] with a fresh HLC timestamp, making it causally after the original
edit and safe to propagate to peers:
// Apply an edit, then undo it.
delta, _ := node.Edit(func(v *MyDoc) { v.Title = "Draft" })
undo := node.Reverse(delta) // applies inverse locally, returns undo delta
// Redo: reverse the undo.
redo := node.Reverse(undo)Export your Deep patches to standard RFC 6902 JSON Patch format, and parse them back:
jsonData, err := patch.ToJSONPatch()
// Output: [{"op":"replace","path":"/name","value":"Bob"}]
restored, err := deep.ParseJSONPatch[User](jsonData)JSON deserialization note: When a patch is JSON-encoded and then decoded, numeric values in
Operation.OldandOperation.Neware unmarshaled asfloat64(standard Go JSON behavior). GeneratedPatchmethods handle this automatically with numeric coercion. If you use the reflection fallback, be aware of this when inspectingOld/Newdirectly.
v4 used a Recursive Tree Patch model. Every field was a nested patch object. While flexible, this caused high memory allocations and made serialization difficult.
Deep uses a Flat Operation Model. A patch is a simple slice of Operations. This makes patches:
- Portable: Trivially serializable to any format.
- Fast: Iterating a slice is much faster than traversing a tree.
- Composable: Merging two patches is a stateless operation.
Apache 2.0