slices.CompactFunc removes repeated values only when equivalent elements are next to each other. That adjacency rule makes it different from set-style deduplication: equal values separated by another element remain separate entries.
The function accepts a slice and an equality function:
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) SIt compacts the slice in place and returns the resulting shorter slice.
Equality applies to consecutive elements
A slice of records can contain consecutive entries that represent the same logical value even when the complete structs differ. The equality function can express that relation directly.
package main
import (
"fmt"
"slices"
)
type Event struct {
Code string
Source string
}
func main() {
events := []Event{
{Code: "ready", Source: "node-a"},
{Code: "ready", Source: "node-b"},
{Code: "busy", Source: "node-a"},
{Code: "ready", Source: "node-c"},
}
events = slices.CompactFunc(events, func(a, b Event) bool {
return a.Code == b.Code
})
fmt.Println(events)
}The first two ready records form one adjacent run, so only the first remains. The final ready record stays because busy separates it from that run.
CompactFunc preserves the first element of each run. It does not search ahead for later matches and does not reorder values to bring matches together.
Input order determines the result
Compaction reflects the sequence already present in the slice. For data grouped by a relevant key, adjacent compaction can collapse each run without constructing a map.
Consider numeric identifiers:
ids := []int{4, 4, 7, 7, 7, 4}
ids = slices.CompactFunc(ids, func(a, b int) bool {
return a == b
})
fmt.Println(ids) // [4 7 4]The two runs containing 4 remain distinct. If the intended contract is one occurrence per value across the entire slice, adjacent compaction alone does not provide that contract.
Sorting before compaction can group equal values, but that also changes sequence order. Such a combination is valid only when reordered output matches the surrounding data contract.
The original backing array is reused
CompactFunc modifies slice elements rather than allocating a separate result for the compacted sequence. Code holding another slice view over the same backing array can therefore observe changed elements.
values := []string{"a", "a", "b", "b"}
alias := values
values = slices.CompactFunc(values, func(a, b string) bool {
return a == b
})
fmt.Println(values) // [a b]
fmt.Println(alias) // backing storage has been modifiedThe returned slice carries the new length. Retaining the old slice header through alias does not preserve the old contents.
This behavior is useful to account for when slices cross ownership boundaries. If callers require the source sequence to remain unchanged, copy it before compaction.
copyOfValues := slices.Clone(values)
copyOfValues = slices.CompactFunc(copyOfValues, func(a, b string) bool {
return a == b
})The copy gives the compaction operation independent backing storage.
Removed tail elements are cleared
After compaction, elements between the new length and the old length are zeroed in the backing array. This matters for element types containing pointers or other references because discarded entries do not remain retained solely in that unused tail region.
The returned slice still has the compacted length, so ordinary indexing cannot access those cleared positions. A separate alias with the old length can expose the backing-array changes.
For a slice of pointers, the same in-place rule applies:
type Item struct {
ID int
}
a := &Item{ID: 1}
b := &Item{ID: 2}
items := []*Item{a, a, b, b}
items = slices.CompactFunc(items, func(x, y *Item) bool {
return x.ID == y.ID
})The result contains the first pointer from each adjacent equivalent run. The unused tail positions in the backing array are cleared to the zero value for *Item, which is nil.
Equality should describe a stable relation
The equality callback is evaluated as compaction scans the sequence. A useful equality rule should treat values consistently: an element should compare equal to itself, argument order should not change the result, and equivalent chains should not produce contradictory group boundaries.
Field-based equality is a common fit:
records = slices.CompactFunc(records, func(a, b Record) bool {
return a.Key == b.Key
})The callback can also normalize representation before comparison when that normalization has a clear domain meaning. For example, case-insensitive text comparison can treat adjacent spelling variants as one run without rewriting the stored value. The retained element is still the first original value in that run.
Compaction is a sequence operation
slices.CompactFunc is best understood as run compression rather than global duplicate removal. It preserves order, keeps the first element of each equivalent run, changes the shared backing array, and returns a slice with a shorter length when runs collapse.
That boundary makes the operation precise: use it when adjacency itself carries meaning. When uniqueness must span the complete collection, a different data structure or an explicit grouping strategy better represents that requirement.