slices.CompactFunc removes repeated values only when equivalent elements are adjacent. That detail makes it distinct from general deduplication: the function operates on runs, keeps the first element from each run, and leaves separated matches alone.
The custom equality function also allows compaction for structs and for equivalence rules that differ from Go’s == operator.
Compaction is based on neighboring values
The function has this signature:
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) SFor each run in which neighboring elements satisfy eq, the first element remains in the result. Consider case-insensitive string comparison:
package main
import (
"fmt"
"slices"
"strings"
)
func main() {
names := []string{"API", "api", "Worker", "worker", "API"}
names = slices.CompactFunc(names, strings.EqualFold)
fmt.Println(names)
}The resulting slice is:
[API Worker API]The final API remains because it is separated from the first run. CompactFunc does not maintain a set of values encountered earlier in the slice.
This behavior fits ordered event streams, grouped records, normalized tokens, and other data where repeated neighbors carry redundant information but later occurrences remain meaningful.
Custom equivalence can ignore selected fields
Struct values often need an equivalence rule based on only part of each record. A stream of status observations, for example, can retain the first record whenever the status changes while discarding adjacent records with the same status:
package main
import (
"fmt"
"slices"
)
type Observation struct {
Status string
Seq int
}
func main() {
values := []Observation{
{Status: "ready", Seq: 10},
{Status: "ready", Seq: 11},
{Status: "busy", Seq: 12},
{Status: "busy", Seq: 13},
{Status: "ready", Seq: 14},
}
values = slices.CompactFunc(values, func(a, b Observation) bool {
return a.Status == b.Status
})
fmt.Println(values)
}The result contains sequence numbers 10, 12, and 14. The first record from each adjacent status run is retained, so fields outside the equality rule still come from the run’s first element.
That retention rule matters when records carry timestamps, identifiers, or metadata. A comparator that treats two records as equivalent does not merge their fields; it only determines whether the later neighbor is removed.
Sorting changes the meaning of the operation
When equal values are scattered through a slice, sorting before compaction can turn run compaction into a form of deduplication. That transformation is valid only when reordering is acceptable.
For example, case-insensitive sorting followed by case-insensitive compaction groups equivalent strings together:
slices.SortFunc(names, func(a, b string) int {
return strings.Compare(strings.ToLower(a), strings.ToLower(b))
})
names = slices.CompactFunc(names, strings.EqualFold)The sort is not an implementation detail. It changes the sequence itself. For logs, state transitions, ordered measurements, or any data whose position carries meaning, sorting can destroy information even if the compacted values appear cleaner.
When order must remain intact, CompactFunc is best treated as run compression rather than global duplicate removal.
The input slice is modified
CompactFunc works in place and returns a slice with a possibly smaller length. Code should use the returned slice:
values = slices.CompactFunc(values, equal)Keeping only the old slice header would retain the old length and would not represent the compacted result correctly.
The standard library also zeroes elements between the new length and the original length. This prevents removed elements in that region from continuing to hold references through stale slots. The backing array can still be shared with other slices, so in-place mutation remains observable through aliases that refer to the same storage.
If the original data must remain unchanged, clone it before compaction:
copyOfValues := slices.Clone(values)
copyOfValues = slices.CompactFunc(copyOfValues, equal)A nil input remains nil after compaction, matching the function’s documented nilness behavior.
Equality rules need consistent semantics
The eq function defines the boundary between runs. Straightforward predicates such as matching IDs, matching normalized names, or matching selected struct fields make those boundaries predictable.
A predicate that depends on mutable external state can produce results that are difficult to interpret because neighboring comparisons may not apply the same rule. The same concern applies to predicates with side effects. Keeping eq as a stable equivalence test makes the resulting runs correspond to a clear property of the data.
slices.CompactFunc is most precise when the data already has meaningful adjacency and equality needs domain-specific logic. If the requirement is instead to remove every repeated value regardless of position, a set-based pass or an order-safe indexing strategy expresses a different operation and should be chosen explicitly.