How to Use the slices and maps Packages (Generic Standard Library)

Use the slices and maps packages to perform common operations like sorting, copying, and searching on Go slices and maps with generic functions.

How to Use the slices and maps Packages

You are building a dashboard. The backend returns a map of feature flags keyed by user ID. The frontend needs the flags sorted alphabetically by ID. You write a loop to extract the keys, call sort.Strings, and iterate again to build the response. It works. You copy-paste the same extraction-and-sort pattern for a map of timestamps, then for a map of configuration values. The code works, but the repetition is noisy. You spend more time writing boilerplate loops than solving the actual problem. Go 1.21 introduced the slices and maps packages to handle this exact friction. These packages provide generic functions that operate on slices and maps directly, removing the need for manual iteration in common cases.

Concept in plain words

Generics let you write a function once that works with many types. The slices package handles operations on slices, like searching, sorting, and cloning. The maps package handles maps, like cloning, copying, and extracting keys. Both rely on type parameters. When you call slices.Contains, the compiler generates a version of the function tailored to your specific slice type. You get the safety of static typing with the flexibility of reusable code.

The functions cover the most common patterns: checking membership, finding indices, sorting, comparing equality, and cloning data structures. Before 1.21, you had to use sort.Slice with a lambda or write custom loops. The new packages standardize these patterns. They are part of the standard library, require no external dependencies, and follow Go conventions. Generics remove the boilerplate. The compiler handles the types.

Minimal example

Here is the simplest usage: check if a slice contains a value, clone a map, and get sorted keys.

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	// Slice of strings to search
	tags := []string{"go", "rust", "python"}

	// slices.Contains returns true if the value exists in the slice
	hasGo := slices.Contains(tags, "go")
	fmt.Println("Has Go?", hasGo)

	// Map of configuration values
	config := map[string]int{
		"timeout": 30,
		"retries": 3,
	}

	// maps.Clone creates a deep copy of the map structure
	// Values are copied by value, so the new map is independent
	backup := maps.Clone(config)

	// maps.Keys extracts all keys into a slice
	// slices.Sorted sorts the slice and returns a new sorted copy
	sortedKeys := slices.Sorted(maps.Keys(config))
	fmt.Println("Sorted keys:", sortedKeys)
}

What happens at compile and runtime

When the compiler sees slices.Contains(tags, "go"), it instantiates the generic function for []string. The generated code runs a linear scan. If the slice is unsorted, the search takes time proportional to the length. maps.Clone allocates a new map and copies every key-value pair. The copy is deep for the map structure itself, meaning the new map has its own backing storage, but the values are copied by value. If the values are pointers, both maps point to the same underlying objects.

slices.Sorted takes the slice of keys, sorts them using the default comparison, and returns a new slice. The original slice remains unchanged. These functions prioritize safety and clarity over micro-optimizations. They allocate new data rather than mutating in place, which prevents accidental side effects. The performance is comparable to hand-written loops because the compiler inlines the generic code. You pay no runtime penalty for using generics.

The comparable constraint

Generic functions enforce constraints. slices.Contains, slices.Sorted, and slices.Equal require the element type to be comparable. The comparable constraint is a predeclared interface that requires the type to support the == and != operators. Basic types like int, string, bool, and pointers are comparable. Structs are comparable if all their fields are comparable.

Slices, maps, and functions are not comparable. Go does not support equality comparison for those types because it can be expensive and ambiguous. If you try to use slices.Contains on a slice of maps, the compiler rejects the code with an error like type map[string]int does not satisfy comparable. You cannot compare maps for equality directly. If you need to check membership in a slice of non-comparable types, you must write a custom loop or convert the data to a comparable representation. If the type cannot be compared, you must write the loop yourself.

Custom sorting with SortFunc

slices.Sort uses the default ordering. For structs, you often need to sort by a specific field. slices.SortFunc accepts a comparison function that returns an integer. Negative means less, zero means equal, positive means greater. This replaces the older sort.Slice pattern. The function signature is cleaner and integrates with the generic ecosystem.

package main

import (
	"fmt"
	"slices"
)

type User struct {
	Name string
	Age  int
}

func main() {
	users := []User{
		{Name: "Charlie", Age: 25},
		{Name: "Alice", Age: 30},
		{Name: "Bob", Age: 25},
	}

	// SortFunc sorts in place using a custom comparison
	slices.SortFunc(users, func(a, b User) int {
		// Compare by name first
		switch {
		case a.Name < b.Name:
			return -1
		case a.Name > b.Name:
			return 1
		}

		// Names are equal, compare by age
		return a.Age - b.Age
	})

	fmt.Println(users)
}

The comparison function defines the order. SortFunc gives you control.

Comparing data structures

slices.Equal checks if two slices have the same length and identical elements. maps.Equal checks if two maps have the same keys and values. These functions are useful for testing and cache invalidation. They handle the iteration and comparison logic for you.

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	a := []int{1, 2, 3}
	b := []int{1, 2, 3}
	c := []int{1, 2, 4}

	// slices.Equal returns true if slices are identical
	fmt.Println("a == b?", slices.Equal(a, b))
	fmt.Println("a == c?", slices.Equal(a, c))

	x := map[string]int{"k": 1}
	y := map[string]int{"k": 1}
	z := map[string]int{"k": 2}

	// maps.Equal returns true if maps have same keys and values
	fmt.Println("x == y?", maps.Equal(x, y))
	fmt.Println("x == z?", maps.Equal(x, z))
}

Min and Max

slices.Min and slices.Max find the smallest and largest elements. They require the type to be ordered, which is a subset of comparable. You can also pass a custom comparison function to MinFunc and MaxFunc. These functions return the value and a boolean indicating if the slice was empty.

package main

import (
	"fmt"
	"slices"
)

func main() {
	numbers := []int{42, 17, 89, 3}

	// Min returns the smallest element and true if slice is non-empty
	min, ok := slices.Min(numbers)
	if ok {
		fmt.Println("Min:", min)
	}

	// Max returns the largest element
	max, _ := slices.Max(numbers)
	fmt.Println("Max:", max)
}

Realistic example

Real code often involves filtering data or merging configurations. Suppose you have a list of active user IDs and a map of user profiles. You need to build a response containing only the profiles for active users, sorted by ID.

// FilterProfiles returns profiles for active users only
func FilterProfiles(activeIDs []string, allProfiles map[string]UserProfile) map[string]UserProfile {
	// Pre-allocate map to avoid resizing during insertion
	result := make(map[string]UserProfile, len(activeIDs))

	for _, id := range activeIDs {
		// Check existence to avoid zero-value insertion
		if profile, exists := allProfiles[id]; exists {
			result[id] = profile
		}
	}

	return result
}
// MergeMaps combines two maps, with overrides taking precedence
func MergeMaps(base, overrides map[string]int) map[string]int {
	// Clone base to create an independent copy
	merged := maps.Clone(base)

	// Apply overrides on top of the base values
	for k, v := range overrides {
		merged[k] = v
	}

	return merged
}
func main() {
	activeIDs := []string{"user-3", "user-1"}
	profiles := map[string]UserProfile{
		"user-1": {Name: "Alice", Role: "admin"},
		"user-3": {Name: "Charlie", Role: "editor"},
	}

	filtered := FilterProfiles(activeIDs, profiles)

	// Sort keys for deterministic JSON output
	sortedIDs := slices.Sorted(maps.Keys(filtered))
	fmt.Println("Active IDs:", sortedIDs)
}

The maps package does not provide a Merge function. You clone the base and range over the overrides. This pattern is explicit and easy to reason about. The community prefers this over a hypothetical maps.Merge because it makes the precedence rules clear. gofmt sorts imports alphabetically. You will see maps before slices in the import block. The tool handles this automatically. The slices package returns concrete slices, not interfaces. This follows the Go mantra "accept interfaces, return structs." Functions return specific types so the caller knows exactly what they have. Clone the base. Range the overrides. Explicit beats implicit.

Pitfalls and compiler errors

Generic functions enforce constraints. slices.Contains and slices.Sorted require the element type to be comparable. You cannot use these functions on slices of maps, slices of functions, or slices of slices. The compiler rejects the code with an error like type map[string]int does not satisfy comparable. If you need to check membership in a slice of non-comparable types, you must write a custom loop or convert the data to a comparable representation.

maps.Clone performs a shallow copy of the values. If your map holds pointers, the new map contains copies of the pointers, not copies of the underlying data. Modifying the data through the cloned map affects the original. Use maps.Clone when you need an independent map structure, but be aware that pointer values still share state.

slices.Index returns the index of the first occurrence, or -1 if not found. Always check the return value against -1. A common bug is using the index directly without validation, which leads to out-of-bounds panics.

slices.Sorted allocates a new slice. Do not use it in a tight loop for huge slices if performance is critical. slices.Sort mutates in place and avoids allocation. maps.Copy copies keys and values from a source map to a destination map. Use maps.Copy when you have a pre-allocated destination map. Check the index. Watch the allocation. Verify the constraint.

When to use slices and maps

Use slices.Contains when you need a quick membership check on a small or medium slice and readability matters more than raw speed. Use a map lookup when you perform many membership checks on the same data, as map lookups are constant time. Use slices.Sorted when you need a sorted copy of a slice without modifying the original data. Use slices.Sort when you can mutate the slice in place and want to avoid allocation. Use maps.Clone when you need a duplicate map to modify independently. Use manual iteration when you need to merge maps with custom conflict resolution logic. Use slices.Compact when you need to remove consecutive duplicates from a sorted slice. Use a custom loop when your data type is not comparable and you need to define equality yourself. Use slices.MinFunc when you need to find the minimum element based on a derived value. Pick the tool that matches the constraint. Simple code wins.

Where to go next