Go to content
Web development

Using Go’s Standard Maps and Slices Packages

Go 1.21 added two generic helper packages to the standard library: slices and maps. Before that, these utilities lived in golang.org/x/exp, an experimental module where the Go team previewed features that might later move into the standard library. Generic means the functions work with any element type, so slices.Sort sorts a []int the same way it sorts a []string, and you don’t write a separate implementation for each type. The new packages give you type-safe functions for the slice and map operations you probably write by hand today: cloning, copying, sorting, searching, deleting, and comparing.

The packages arrived in two stages. Go 1.21 brought the core functions built on generics, along with new built-ins min, max, and clear. A later Go release added iterator-based functions such as maps.Keys and slices.All, which yield values one at a time and work directly in a range loop. If you still use the x/exp versions, use the standard library packages for new code; the migration is mostly mechanical. Here is the before-and-after code for the patterns these helpers replace, along with the behavioral differences to watch for when you switch.

Cloning, copying, and comparing collections

If you copy slices or maps by hand, the clone helpers remove the boilerplate and prevent one subtle bug: forgetting to preallocate the destination with the right length.

maps.Clone returns a shallow copy of the map. It sets new keys and values with ordinary assignment, so any values that are pointers or reference types are shared between the original and the clone. slices.Clone also copies each element into a new backing array, the underlying array that a slice references, but values the elements point to are shared.

// Before: clone a map by hand
clone := make(map[string]int, len(m))
for k, v := range m {
    clone[k] = v
}

// After
clone := maps.Clone(m)
// Before: copy a slice
clone := make([]string, len(names))
copy(clone, names)

// After
clone := slices.Clone(names)

maps.Copy does for maps what the built-in copy does for slices: it copies every key/value pair from src into dst, overwriting the value for any key that already exists in dst.

// Before: merge one map into another
for k, v := range src {
    dst[k] = v
}

// After
maps.Copy(dst, src)

slices.Concat builds a new slice from the contents of several slices and replaces verbose append chains:

// Before
combined := append(append([]string{}, a...), b...)

// After
combined := slices.Concat(a, b)

slices.Clip removes unused capacity, reducing the slice’s capacity to match its length; use it when a slice grew through many appends and now holds far more memory than it needs. slices.Grow does the opposite: it guarantees capacity for a given number of additional elements, so a loop that appends many values does not reallocate repeatedly. slices.Repeat builds a new slice by repeating a value a set number of times, which is handy for initializing test data.

For comparison, maps.Equal reports whether two maps contain the same key/value pairs, comparing values with ==. maps.EqualFunc does the same but compares values with a function you provide, while keys are still compared with ==. One caveat: the maps package has no special handling for non-reflexive keys, keys that do not equal themselves, such as floating-point NaN. Since NaN never equals itself, maps.Equal reports two maps as different if they contain NaN keys, even when the maps were built identically. The slices package has the analogous slices.Equal and slices.Compare.

Sorting and searching with slices

The slices package replaces the older sort package functions for most sorting work. slices.Sort sorts a slice in place using the element type’s < operator, so slices.Sort(nums) does the same job as sort.Ints(nums) and works for any ordered type.

// Before
sort.Ints(nums)

// After
slices.Sort(nums)

For custom orderings, slices.SortFunc takes a comparison function that returns a negative, zero, or positive integer, the same convention as sort.Slice‘s less function but with the two elements passed directly instead of by index. The older sort.Slice, sort.SliceStable, and sort.SliceIsSorted are less ergonomic and slower than their slices counterparts. They use reflection (runtime type inspection) to work with any slice type; the generic versions know the element type at compile time. If you are migrating sorting code, slices.SortStableFunc and slices.IsSortedFunc are the direct replacements for the stable and sorted-check variants.

// Before
sort.Slice(users, func(i, j int) bool {
    return users[i].Age < users[j].Age
})

// After
slices.SortFunc(users, func(a, b User) int {
    return a.Age - b.Age
})

slices.BinarySearch returns the index where a value would be inserted and whether the value is present; it requires a sorted slice. The old sort.SearchInts style forced you to check the index yourself.

// Before
idx := sort.SearchInts(nums, target)
if idx < len(nums) && nums[idx] == target {
    // found
}

// After
idx, found := slices.BinarySearch(nums, target)

For linear search, slices.Contains reports whether a value is present, and slices.Index returns the first index of a value or -1. Both replace short hand-written loops.

// Before
found := false
for _, v := range items {
    if v == target {
        found = true
        break
    }
}

// After
found := slices.Contains(items, target)

slices.Max and slices.Min return the largest and smallest elements, and both panic on an empty slice. slices.IsSorted checks whether a slice is already sorted. Each of these is a one-liner that previously required a loop or a sort package call.

Filtering, compacting, and deleting

The standard library has no slices.Filter or slices.Map, and the Go team has declined to add them. Rob Pike rejected a proposed slices.Filter with the reasoning that "You can just use a for loop, which is more flexible in general." So the intended way to filter a slice is to write a loop that appends to a zero-length slice; the helpers that do exist handle the cases where a loop is error-prone.

slices.Delete removes a range of elements from a slice, shifting the remaining elements left. It takes a start and end index, so deleting one element at index i is slices.Delete(items, i, i+1). The standard library version zeroes the tail elements after the shift, making pointer elements unreachable so the garbage collector can reclaim them. The old x/exp version left them reachable, a memory leak for slices of pointers.

// Before
items = append(items[:i], items[i+1:]...)

// After
items = slices.Delete(items, i, i+1)

If you delete from the front of a slice, each deletion shifts the remaining elements and takes O(n) per call. The Go issue tracker documents the low performance that results from this common misuse of Delete. If you need to remove many elements from the front, consider deleting a range at once or building a new slice.

slices.Compact removes consecutive duplicate elements, keeping the first of each run. It only removes consecutive duplicates, so sort the slice first if you want all duplicates gone. slices.CompactFunc does the same with a custom equality function.

// Before
out := items[:0]
for i, v := range items {
    if i == 0 || v != items[i-1] {
        out = append(out, v)
    }
}

// After
items = slices.Compact(items)

slices.Replace swaps a range of elements for new ones, and slices.Reverse reverses a slice in place. For maps, maps.DeleteFunc deletes every key/value pair for which the provided function returns true. It replaces the pattern of ranging over a map and calling delete inside the loop. Deleting during a range is safe in Go, but the helper makes the intent explicit and removes the loop.

// Before
for k, v := range m {
    if shouldDelete(k, v) {
        delete(m, k)
    }
}

// After
maps.DeleteFunc(m, func(k string, v int) bool {
    return shouldDelete(k, v)
})

Iterator-based collection functions

A later Go release added iterator-based functions to both packages. An iterator here is a function that yields values one at a time, and Go’s range loop can consume it directly. The maps package provides maps.Keys, maps.Values, and maps.All, which iterate over keys, values, and key/value pairs. The slices package has three variants: slices.All yields index/value pairs, slices.Backward yields the same pairs in reverse, and slices.Values yields values only.

All of these share the same rule: iteration order is not specified and is not guaranteed to be the same from one call to the next. For maps this is the same ordering guarantee you already get from ranging over a map, so sort the results whenever order matters.

The iterator functions work with collector functions, which gather the yielded values into a slice or map. slices.Sorted consumes an iterator and returns a sorted slice, which turns the common "collect map keys and sort them" loop into one line:

// Before
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

// After
keys := slices.Sorted(maps.Keys(m))

slices.SortedFunc and slices.SortedStableFunc sort the same way but take a custom comparison function. slices.Collect gathers iterator values into a new slice, and slices.AppendSeq appends them to an existing slice.

maps.Collect builds a new map from an iterator of key/value pairs, and maps.Insert adds such pairs to an existing map and overwrites values for keys that already exist. Combined with slices.All, maps.Collect replaces a four-line loop you would write to build a map from a slice’s indexes to its values:

// Before
indexed := make(map[int]string)
for i, v := range items {
    indexed[i] = v
}

// After
indexed := maps.Collect(slices.All(items))

slices.Chunk returns an iterator over non-overlapping sub-slices of a given size, so you can process items in batches:

for batch := range slices.Chunk(items, 100) {
    process(batch)
}

Migrating from golang.org/x/exp

If you already use golang.org/x/exp/slices or golang.org/x/exp/maps, most of the migration is changing the import path, because the standard library packages kept the same function names and signatures. Large projects have tracked exactly this change; the Terraform AWS provider, for example, has an open issue for replacing its x/exp maps and slices usage with the standard library. The behavioral differences need care during migration.

The first difference is slices.Delete. The x/exp implementation had a memory leak: when it removed M elements, the M values at the tail of the backing array stayed reachable through the original slice, so pointer elements were not garbage collected. The standard library version zeroes those tail values. The x/exp version also lacked a check that the start index is less than or equal to the end index. If you called it with an invalid range, it silently produced a result that violated the function’s documentation. The standard library version panics on i > j. If your code ever passed an invalid range and relied on the old non-panicking behavior, it will now fail loudly, which is the intended signal that the call was wrong.

The second difference depends on your Go version. A segmentation violation in maps.Clone was reported in Go 1.22.5 under stress testing. If you are on that release, upgrade to a fixed version before relying on maps.Clone in long-running or heavily exercised code paths.

The third difference is tooling. The gopls language server’s modernize refactoring can suggest replacing maps.Clear with the built-in clear function when your go.mod declares Go 1.21 or newer. The same tooling has produced rewrites that change program behavior: a proposed rewrite of a loop to slices.Contains dropped an empty-slice check in one reported case. Automated fixes are worth reviewing, especially when the original loop does anything beyond the single operation the rewrite targets.

Finally, Go 1.21 added clear as a built-in: clear(m) deletes every entry from a map, and clear(s) zeroes the elements of a slice without changing its length. For maps, this is the same operation as maps.Clear from x/exp, which is why the modernizer suggests the switch.

More in Web development Back to top ↑