If a Go slice contains repeated values next to each other, you don’t need to write an index-heavy loop to collapse them. Since Go 1.21, slices.Compact handles that operation directly for comparable element types.
The word consecutive matters. Given []string{"api", "api", "web", "api"}, the result is []string{"api", "web", "api"}. The last "api" stays because it belongs to a different run. slices.Compact isn’t a general-purpose “unique values” function.
What slices.Compact actually does
slices.Compact replaces each consecutive run of equal elements with its first element. It modifies the slice’s backing array and returns a slice with the resulting length.
package main
import (
"fmt"
"slices"
)
func main() {
services := []string{"api", "api", "web", "web", "worker", "api"}
services = slices.Compact(services)
fmt.Println(services)
}The output is:
[api web worker api]The two "api" runs remain separate. This behavior is useful when repetition has already been grouped, or when adjacent repetition itself is the noise you want to remove.
The function works with comparable element types, so strings, integers, booleans, pointers, and structs whose fields are all comparable can be used directly.
Keep the returned slice
Compact can reduce the logical length of a slice, so its return value is part of the operation. This call is wrong:
slices.Compact(services)
fmt.Println(services) // still has the original lengthThe backing array has been modified, but services still has its old slice header and therefore its old length. Assign the result back:
services = slices.Compact(services)This is the same habit you need with other length-changing slice operations such as append and slices.Delete.
Since Go 1.22, Compact also zeroes the elements between the new length and the original length. That detail matters for slices containing pointers or other values that can retain referenced objects. It also makes continuing to use the old, longer slice especially misleading: its tail no longer contains useful pre-compaction values.
Sorting turns adjacent compaction into value deduplication
Sometimes the real requirement is “keep each value once,” regardless of where duplicates appear. slices.Compact alone doesn’t promise that.
For ordered values, sorting first groups equal values together:
package main
import (
"fmt"
"slices"
)
func main() {
ids := []int{7, 2, 7, 3, 2, 2}
slices.Sort(ids)
ids = slices.Compact(ids)
fmt.Println(ids)
}Output:
[2 3 7]This is compact and readable when sorted output is acceptable. It is not a drop-in replacement for stable deduplication, because sorting changes the original order.
If input order carries meaning, use a seen-set instead:
func uniqueStable[T comparable](values []T) []T {
seen := make(map[T]struct{}, len(values))
out := make([]T, 0, len(values))
for _, value := range values {
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}That solves a different problem. Choosing between the two approaches should start with whether duplicates must be adjacent and whether order may change.
Use slices.CompactFunc for custom equality
Not every useful notion of “same” matches Go’s == operator. slices.CompactFunc accepts an equality function and works with element types that aren’t comparable as well.
A common example is case-insensitive text:
package main
import (
"fmt"
"slices"
"strings"
)
func main() {
names := []string{"API", "api", "Web", "WEB", "worker"}
names = slices.CompactFunc(names, strings.EqualFold)
fmt.Println(names)
}The result keeps the first element from each equal run:
[API Web worker]That “keep the first” behavior can be useful when the original spelling or representation should survive.
For structs, the equality function can deliberately compare only the field that defines a duplicate:
type Event struct {
ID string
Payload []byte
}
events = slices.CompactFunc(events, func(a, b Event) bool {
return a.ID == b.ID
})Here Event contains a slice and therefore can’t be passed to slices.Compact, but CompactFunc can still collapse adjacent events with the same ID.
Be careful about what that equality means. If two adjacent events share an ID but carry different payloads, this code silently keeps the first payload and discards the second event. That’s correct only if the ID really defines equivalence for this operation.
Compaction modifies the backing array
A slice isn’t an owned container; it is a view over an array. slices.Compact rearranges elements in that array in place.
That can surprise code holding another slice that overlaps the same backing array:
values := []string{"a", "a", "b", "b"}
alias := values
values = slices.Compact(values)
fmt.Println(values)
fmt.Println(alias)values has the compacted length, while alias still has the old length and observes the modified backing storage. With current Go versions, the obsolete tail is zeroed, so alias may contain zero-value strings after the compacted prefix.
If the original contents must remain untouched, clone before compacting:
compacted := slices.Clone(values)
compacted = slices.Compact(compacted)The extra allocation is intentional: it buys independence from the original backing array.
Nil and empty slices need no special case
slices.Compact preserves whether its input is nil. A nil slice stays nil, while an empty non-nil slice remains an empty slice.
That means callers generally don’t need guards such as:
if values != nil && len(values) > 0 {
values = slices.Compact(values)
}Just compact the slice:
values = slices.Compact(values)For ordinary application code, this keeps the control flow focused on the actual operation instead of slice-state bookkeeping.
Common mistakes come from asking it to solve a different problem
Most misuse of slices.Compact isn’t about syntax. It’s about assuming stronger semantics than the function provides.
Don’t use it by itself to remove duplicates scattered throughout an unsorted slice. Don’t sort first when preserving encounter order matters. Don’t ignore the returned slice, and don’t assume another alias to the same backing array remains unchanged.
CompactFunc deserves one more caution: the equality callback should represent a sensible equivalence relation for adjacent values. A stateful callback or one whose answer changes between calls makes the result difficult to reason about.
Pick compaction when runs are the thing you want to remove
slices.Compact fits naturally when your data already contains runs: sorted IDs, repeated state observations, normalized tokens, or adjacent duplicate records. It expresses the operation more clearly than a handwritten read/write-index loop and handles the shortened slice for you.
Before reaching for it, state the requirement precisely: “remove adjacent duplicates” is different from “return every distinct value.” If the former is what you mean, assign the returned slice and use slices.Compact. If equality needs domain rules, move to slices.CompactFunc. If duplicates can appear anywhere and order matters, a seen-set is usually the clearer tool.