Documentation
¶
Overview ¶
Package merge layers one configuration value over another, and deep-copies one, from the struct definitions themselves.
It exists to delete a category of code: hand-written `if o.X != "" { s.X = o.X }` chains, hand-written emptiness tests, and hand-written field-by-field copies. Each of those has to be updated whenever a field is added, and each is a place a field can be silently forgotten. Structural merging cannot forget a field, so adding one to a spec needs no merge code at all.
The default behaviour is:
- scalars and strings — the override wins when it is set (a zero value means "unset", so an override can turn a bool on but never off);
- slices — replaced wholesale when the override's is non-empty, because a list of tools means exactly those tools;
- maps — merged key-wise, because a map is a set of independent settings;
- structs, including those behind a pointer — merged field by field, so setting one sub-field does not erase its siblings.
A single field can name an exception the structure cannot express, with a tag:
Pre []string `merge:"append"` // the base's elements, then the override's Allow []string `merge:"append,unique"` // as append, with repeats dropped
Tags are read on struct fields reached through structs and through pointers that are non-nil on both sides. A tag that cannot mean what it says — append on something that is not a list, unique over a non-comparable element type, a rule this package does not define — panics rather than being ignored, because a silently ignored tag reads exactly like an honoured one.
Policy names the exceptions that belong to a whole type rather than to one field, including the types whose merge is a domain rule rather than a structural one and which therefore merge themselves.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Apply ¶
Apply returns base with override's set fields layered on top, per the package default and policy's exceptions. Neither argument is mutated, and the result shares no mutable memory with either — a merged spec can be edited without reaching back into the config it inherited from.
T must be a struct, map or slice type; anything else is a caller error and panics, as does a mergo failure, which cannot occur for a well-formed T since both operands are the same concrete type by construction.
Types ¶
type Policy ¶
type Policy struct {
// Replace lists types that are indivisible: when the override's value is set
// it replaces the base's wholesale instead of being merged into. A pointer
// type counts as set when it is non-nil — which is how an explicit
// *float64(0) or *bool(false) survives a merge that would otherwise read the
// pointed-at zero as "unset" and drop it.
//
// Name a type by its zero value: merge.Policy{Replace: []any{(*float64)(nil)}}.
Replace []any
// Replace, and Clone copies the reference instead of walking it. Registries,
// catalogs and caches belong here — deep-copying one is both wasteful and
// wrong, because consumers rely on pointer identity.
Shared []any
// Merger lists types that merge themselves. Each must declare the method
// `Merge(T) T` on the type named; Apply calls base.Merge(override) and takes
// the result instead of walking the type. It is where a merge rule that is a
// domain decision rather than a structural one lives: a list that accumulates
// across configuration layers instead of being replaced, or two fields that
// only mean anything when they move together.
//
// The method always runs — a nil slice or map destination is materialised
// first, so "the base had none" reaches Merge as an empty value rather than
// silently bypassing it. A pointer type therefore cannot be a Merger (a nil
// pointer has nothing to call the method on), and naming a type without that
// exact method panics rather than quietly reverting to structural merging.
Merger []any
}
Policy declares the exceptions to structural merging for one family of types. The zero Policy is valid and means "no exceptions".