Go to content
Web development

Migrating from golang.org/x/exp/maps and slices to the Go Standard Library

Go 1.21 added the generic slices and maps packages to the standard library. Before that, the same utility functions lived in golang.org/x/exp, the experimental module where the Go team previews features before committing to them. For most call sites, you only swap the import path, because the standard library kept the same function names and signatures for the functions it adopted. But a handful of functions changed return types or fixed old behavior. You need to know which ones before you run a mechanical find-and-replace.

The migration comes down to three things: what carries over unchanged, where the standard library version behaves differently from x/exp, and which tools can find and rewrite the usage. "Generic" here means the functions work with any element type, so slices.Sort can sort []int, []string, or any other ordered type without you writing a separate implementation. An "iterator" is a function that yields values one at a time, and Go’s range loop can consume it directly.

Most call sites only change the import path

The standard library packages kept the same function names and signatures as their x/exp counterparts, so most of the migration is swapping golang.org/x/exp/slices for slices and golang.org/x/exp/maps for maps. The functions maps.Clone, maps.Copy, maps.Equal, maps.EqualFunc, and maps.DeleteFunc carry over unchanged, as do slices.Sort, slices.SortFunc, slices.Contains, slices.Index, slices.BinarySearch, slices.Compact, slices.Concat, slices.Clip, slices.Grow, and slices.Repeat.

Several of these replace older idioms, and the replacements are worth knowing even if you are not migrating existing code. slices.Sort(nums) sorts a slice in place using the element type’s < operator, so it does the same job as sort.Ints(nums) for []int and works for any ordered type. slices.SortFunc takes a comparison function that returns a negative, zero, or positive integer, the same convention as sort.Slice‘s less function, except it receives the two elements directly instead of their indexes. The generic versions are also faster than the older sort.Slice family. The older functions use reflection, which is runtime type inspection, to handle any slice type. The generic functions know the element type at compile time.

Two functions in this group carry a caveat. maps.Equal compares values with ==, and maps.EqualFunc compares them with a function you provide while still comparing keys with ==. Neither handles floating-point NaN keys specially, and since NaN never equals itself, maps.Equal reports two maps as different when they contain NaN keys even if you built the maps identically. And slices.BinarySearch returns the index where a value would be inserted and whether the value is present, but it requires a slice that is already sorted.

The iterator functions return a different type

The biggest breaking change is in the functions that produce collections. A later Go release added iterator-based functions to both packages: maps.Keys, maps.Values, and maps.All iterate over keys, values, and key/value pairs, and the slices package has slices.All (index/value pairs), slices.Backward (the same pairs in reverse), and slices.Values (values only). In x/exp, maps.Keys and maps.Values returned slices. In the standard library they return iterators. Code that ranges over the result still works, but code that assigns the result to a slice variable stops compiling.

To get a slice back, wrap the iterator with slices.Collect, or use slices.Sorted when you need the results in order:

keys := slices.Collect(maps.Keys(m))
sortedKeys := slices.Sorted(maps.Keys(m))

That second line replaces the common collect map keys and sort them loop with a single expression. The same rules apply to the rest of the iterator helpers. slices.Collect gathers 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. maps.Insert adds such pairs to an existing map and overwrites values for keys that already exist. slices.Chunk returns an iterator over non-overlapping sub-slices of a given size for batch processing.

All of these functions share one rule: iteration order is not specified and is not guaranteed to be the same from one call to the next. For maps, that is the same ordering guarantee you already get from ranging over a map, so sort the results whenever order matters. A file that uses maps.Keys or maps.Values from the standard library needs Go 1.23, while the rest of the migration needs only Go 1.21.

Behavior differences that break existing code

Two functions changed behavior in ways that affect correctness, not just compilation. The first is maps.Clear. The standard library migration recipe turns maps.Clear(m) into the built-in clear(m), which Go 1.21 added alongside the new packages. clear(m) deletes every entry from a map, and on a slice it zeroes the elements without changing the length. If your code calls maps.Clear, the rewrite is simple, but it is a real API change rather than an import swap.

The second is slices.Delete, which removes a range of elements and shifts the rest left; deleting one element at index i is slices.Delete(items, i, i+1). The standard library version fixes two defects in the x/exp version. It zeroes the tail elements after the shift. This makes pointer elements unreachable so the garbage collector can reclaim them. The old version left them reachable, which leaked memory for slices of pointers. It also panics when the start index is greater than the end index. The old version silently produced a result that violated the function’s documentation. Code that passed invalid ranges now fails loudly, and code that held on to pointer slices uses less memory.

One thing the standard library still does not give you is slices.Filter or slices.Map. 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." If your code uses an experimental Filter or Map from a fork or another package, rewrite those call sites with a loop, because no standard library equivalent is coming.

Automated tools that find and rewrite the migration

You do not have to do this by hand. The gopls language server’s modernize refactoring can suggest replacing maps.Clear with the built-in clear when your go.mod declares Go 1.21 or newer. The Code Genome Project’s MigrateXExpMapsToStdlib recipe handles the same package more broadly: it leaves Clone, Copy, Equal, EqualFunc, and DeleteFunc as they are, turns Clear(m) into the clear(m) builtin, and wraps Keys(m) and Values(m) as slices.Collect(maps.Keys(m)).

For finding what you have, the FindXExpUsage recipe marks every golang.org/x/exp/slices, /maps, and /constraints call and type reference with its standard-library replacement, and warns that the numeric constraints and a boolean comparator passed to a sort function have no replacement. golangci-lint v2.12.2 enables the govet inline analyzer, which reports deprecated experimental maps and slices package usage.

In one reported case, a gopls rewrite of a loop to slices.Contains dropped an empty-slice check. Whatever tool you use, review its output, because automated fixes have produced rewrites that change program behavior. Run your tests after the migration and read each rewrite before you accept it.

Under stress testing, maps.Clone crashed with a segmentation violation in Go 1.22.5, so upgrade to a fixed release before relying on it in long-running or heavily exercised code paths.

More in Web development Back to top ↑