Duplicate values often arrive in runs: repeated status events, sorted IDs, or adjacent tokens produced by a parser. When only consecutive duplicates need to disappear, slices.Compact handles the operation without a handwritten loop.

The distinction is specific. slices.Compact collapses adjacent equal values; it doesn’t search the entire slice for duplicates. It also modifies the slice’s backing storage, so callers need to account for aliasing.

Remove adjacent duplicates with slices.Compact

Pass a slice whose element type is comparable and assign the returned slice:

package main

import (
    "fmt"
    "slices"
)

func main() {
    states := []string{"queued", "queued", "running", "running", "done"}

    states = slices.Compact(states)

    fmt.Println(states)
}

The output is:

[queued running done]

Each consecutive run contributes one value to the result. The order of retained values stays the same.

Assigning the result matters because compaction can reduce the slice length. Calling the function and ignoring its return value leaves the original slice header unchanged:

values := []int{1, 1, 2, 2}

slices.Compact(values)

fmt.Println(values)

The backing array has been modified, but values still has its original length. Use values = slices.Compact(values) when the shorter logical slice is the desired result.

Non-adjacent duplicates remain

slices.Compact is not general-purpose deduplication. Equal values separated by another value form different runs:

values := []int{4, 4, 7, 4, 4}

values = slices.Compact(values)

fmt.Println(values) // [4 7 4]

Both groups of 4 remain represented. This behavior fits data that is already grouped by value or cases where only repeated neighbors are noise.

If every duplicate should be removed and element order doesn’t already group equal values, another strategy is required. For comparable values, a map can track values already seen:

func unique(values []string) []string {
    seen := make(map[string]struct{}, len(values))
    out := make([]string, 0, len(values))

    for _, value := range values {
        if _, exists := seen[value]; exists {
            continue
        }
        seen[value] = struct{}{}
        out = append(out, value)
    }

    return out
}

That operation has different semantics and allocation behavior. It removes repeated values across the whole input and creates separate result storage.

Sorting first changes the meaning of the result

Sorting before compaction is a concise way to remove all duplicate ordered values:

numbers := []int{8, 3, 8, 5, 3}

slices.Sort(numbers)
numbers = slices.Compact(numbers)

fmt.Println(numbers) // [3 5 8]

This is suitable when sorted output is acceptable. It isn’t a drop-in replacement for stable deduplication because the original order is lost.

If the input order carries meaning, such as event arrival order or user-selected priorities, sorting just to make duplicates adjacent can silently change the data contract. A seen-value map is usually a better fit when first-occurrence order must be retained.

Compaction modifies the backing array

The operation works in place. Any slice sharing the same backing array can observe changes.

values := []string{"a", "a", "b", "b"}
alias := values

values = slices.Compact(values)

fmt.Println(values) // [a b]
fmt.Println(alias)  // backing storage has changed

The exact contents visible through the longer alias shouldn’t be treated as preserved input. Current Go documentation specifies that elements between the new length and the original length are zeroed.

When the source must remain unchanged, clone it first:

source := []string{"a", "a", "b", "b"}

result := slices.Clone(source)
result = slices.Compact(result)

fmt.Println(source) // [a a b b]
fmt.Println(result) // [a b]

The clone makes ownership explicit. This is useful when a function receives a caller-owned slice but needs a compacted working copy.

Nil and empty slices need no guard

A nil input stays nil after compaction:

var values []int

values = slices.Compact(values)

fmt.Println(values == nil) // true

An empty non-nil slice remains empty. There is no need to check len(values) before calling slices.Compact.

Preserving nilness can matter when later code distinguishes absent data from an allocated empty collection. The function doesn’t introduce an allocation just to represent an empty result.

Element values must be comparable

slices.Compact uses equality on adjacent elements, so its element type has the comparable constraint. Strings, integers, pointers, and structs made entirely from comparable fields fit directly.

A struct containing a slice does not:

type Record struct {
    ID   int
    Tags []string
}

For values like this, or whenever equality is domain-specific, use slices.CompactFunc. The callback decides whether adjacent elements belong to the same run:

type Record struct {
    ID   int
    Tags []string
}

records := []Record{
    {ID: 10, Tags: []string{"new"}},
    {ID: 10, Tags: []string{"retry"}},
    {ID: 20, Tags: []string{"new"}},
}

records = slices.CompactFunc(records, func(a, b Record) bool {
    return a.ID == b.ID
})

fmt.Println(len(records)) // 2

For each run that compares equal, slices.CompactFunc keeps the first element. Here that means the record with ID: 10 and tag new survives while the adjacent retry record is removed.

Use compaction when adjacency is the rule

slices.Compact is a good fit when repeated neighbors are redundant and mutating the supplied storage is acceptable. Assign its return value, and clone first when the caller’s original slice must stay intact.

For duplicates scattered across the input, choose a method that matches the required ordering. A map can retain first occurrences, while sorting followed by compaction can produce a sorted set-like result. Keeping those semantics explicit prevents a short deduplication step from changing more data than intended.