How to remove duplicates from a slice in Go
Walk the slice once with a seen-set: seen := make(map[T]struct{}), skip anything already in it, append the rest to a fresh slice. That is O(n) and keeps the original order. If you don't care about order, slices.Sort(s) then s = slices.Compact(s) — but Compact alone only removes adjacent duplicates.
Go has no set type and no Unique function, so deduplication is something you assemble from two pieces you already have: a map, which gives you O(1) membership, and append. Everything on this page is a variation on that — or on the opposite trade, sorting first so duplicates end up next to each other and no map is needed at all. Each example runs on this page — hit Run, then edit the code and run it again.
1A map seen-set, in one passRecommended
This is the answer nine times out of ten. The map is the set: map[T]struct{} stores keys and nothing else, because the empty struct occupies zero bytes. Append each value the first time you meet it and you get first-seen order for free — the property Sort + Compact throws away. Since Go 1.18 you can write it once as a generic helper and use it for every comparable element type.
Output
Prints [go rust zig], 6 -> 3, [4 7 1 9], then the unchanged [go rust go zig rust go]. Both make calls are sized up front, which matters: without the hints the map rehashes and the slice regrows several times over a long input. map[T]bool works too and reads a touch shorter (if seen[v] { continue }), at one byte per entry — struct{} is the idiom precisely because a set has no values worth storing. The constraint is comparable, so element types that can't be compared with == — slices, maps, functions — won't compile; key on something derived instead, as in section 4.
2slices.Sort + slices.Compact
slices.Compact (Go 1.21+) is the closest thing the standard library has to a dedupe, and the trap is right in the doc comment: it removes consecutive duplicates only. Hand it unsorted data and it collapses runs and leaves everything else alone. Sort first and every equal pair becomes adjacent, so the pair does the whole job — no map, no second slice, at the cost of O(n log n) and the original order. See sort a slice for the sorting half.
Output
The unsorted call prints [5 2 5 9 2] — only the trailing 2 2 collapsed, so both 5s and two of the three 2s are still there. Sorted, it reads [2 2 2 5 5 9] and compacts to [2 5 9]. Two rules come with this form: Compact returns a shorter slice header and rewrites the backing array in place, so you must assign the result back, and the caller's slice is modified unless you work on slices.Clone(s) — which is why the last line still prints [5 2 5 9 2 2]. Since Go 1.22 Compact zeroes the slots it vacates, so a slice of pointers doesn't leak.
3Custom equality and case-insensitive dedupe
slices.CompactFunc(s, eq) is Compact with your own equality test, so strings.EqualFold gives you a case-insensitive dedupe in one call. It is still adjacent-only, which means the sort in front of it has to agree with it: sort by the same normalised key, or values that eq considers equal will never meet. SortStableFunc rather than SortFunc keeps the input order inside each group, so the spelling that survives is the first one you wrote.
Output
The already-grouped input compacts straight to [Go Rust]. The shuffled one sorts to [go GO Go Rust rust] and compacts to [go Rust] — note the survivors are go and Rust, the first member of each group in the stable order, not the first element of the original slice. If you want case-insensitive dedupe and the original order, skip CompactFunc and go back to section 1 with a normalised key: seen[strings.ToLower(v)] while you append the untouched v.
4Deduping structs by a key field
"Unique users" almost never means "unique whole rows" — it means unique ID. Key the seen-set on the field instead of the element and the rest of the loop is unchanged. Go will also let you key on the whole struct, because a struct is comparable when all of its fields are, which is the version to use when two rows differing in any field are genuinely different.
Output
Prints 1 [email protected], 2 [email protected], 3 [email protected], then distinct rows: 4. The two numbers tell the story: keying on ID gives 3 users — the second Ann row was dropped even though its email differed, because first-seen wins — while keying on the whole value gives 4, since only the duplicated Bob row is identical in every field. Whole-struct keys stop compiling the moment a field is a slice, map, or function; for those, key on a derived string (or a hash) you build yourself. And if you'd rather not allocate a map at all, slices.SortFunc by ID followed by slices.CompactFunc(users, func(a, b User) bool { return a.ID == b.ID }) does the same job in O(n log n).
5In-place dedupe that reuses the backing array
keep := s[:0] is a slice of length 0 over the same array, so appending the survivors writes them over the front of the input as you go. It never allocates for the result — a real win in a hot path — and the write index can never overtake the read index, so the pass is safe. What you give up is the input: by the end, s holds the deduped prefix followed by leftovers. Same trick as the filter in remove an element from a slice.
Output
Prints [4 7 1 9] 4 7 — four elements over the original capacity of seven, proof that nothing was allocated for the result. The full array then reads [4 7 1 9 7 7 9]: the survivors up front, and stale copies of already-emitted values in the tail. For int that is only untidy, but for a []*User or [][]byte those leftovers are live references the garbage collector can't free, which is what clear fixes — after it the array reads [4 7 1 9 0 0 0]. Note the map is still an allocation; this trick removes the second slice, not the set.
6Which should you use?
| Method | Order | Time | Extra memory | Best for |
|---|---|---|---|---|
| map set + new slice | First-seen | O(n) | Map + result | The default answer |
| map set + s[:0] | First-seen | O(n) | Map only | Hot paths where the input is disposable |
| slices.Sort + slices.Compact | Sorted | O(n log n) | None | Sorted output wanted anyway |
| slices.Compact alone | Preserved | O(n) | None | Already-sorted input, or collapsing runs |
| slices.CompactFunc(s, eq) | Sorted | O(n log n) | None | Case-insensitive or field-based equality |
| seen[key(v)] on a struct field | First-seen | O(n) | Map + result | Unique-by-ID over records |
Frequently asked questions
How do I remove duplicates from a slice in Go?
Loop once with a set: seen := make(map[T]struct{}, len(s)), skip any value already in seen, and append the rest to a new slice. That is O(n) and keeps the first occurrence of each value in its original position. The alternative is slices.Sort(s) followed by s = slices.Compact(s), which needs no map but reorders the data.
Does Go have a built-in function to remove duplicates from a slice?
Not a general one. Go has no set type and nothing called Unique. The closest is slices.Compact (Go 1.21+), which removes runs of consecutive equal elements — it deduplicates a slice fully only if that slice is already sorted. For arbitrary input you either sort first or write the map seen-set loop yourself.
Why does slices.Compact leave duplicates in my slice?
Because Compact only removes duplicates that are next to each other. On []int{5, 2, 5, 9, 2, 2} it returns [5 2 5 9 2] — just the trailing pair collapsed. Call slices.Sort(s) first (or slices.SortFunc with the matching comparison) so every equal pair is adjacent, and remember to assign the result back: s = slices.Compact(s).
How do I remove duplicates from a slice while keeping the order?
Use the map seen-set, not sort-and-compact. Appending each value the first time you see it preserves the original order by construction, while slices.Sort destroys it. If you need case-insensitive matching with the order kept, store the normalised value as the key (seen[strings.ToLower(v)]) and append the original v.
How do I remove duplicate structs from a slice in Go?
Key the set on whatever makes two records the same. For unique-by-ID use map[int]struct{} on u.ID; to treat rows as duplicates only when every field matches, use the struct itself as the key — map[User]struct{} compiles as long as all its fields are comparable. Structs containing a slice, map, or function are not comparable, so build a derived string key for those.
Is a map seen-set faster than sort and compact for deduping in Go?
Usually, on big slices: the map pass is O(n) against O(n log n) for the sort. But the map allocates and every lookup hashes, so on small slices — a few dozen elements — or on data that is already nearly sorted, slices.Sort plus slices.Compact is often quicker and allocates nothing. Benchmark your own sizes before optimising either way.
What does map[string]struct{} mean in Go?
It is the idiomatic set: a map whose keys carry the membership and whose values are the empty struct, which occupies zero bytes. Add with seen[v] = struct{}{} and test with _, ok := seen[v]. map[string]bool is equivalent and reads slightly shorter (if seen[v]), at the cost of one byte per entry and the risk of confusing "absent" with "present and false".