How to Use the maps and slices Packages from the Standard Library

The `maps` and `slices` packages are standard library utilities added in Go 1.21 to provide common operations on maps and slices without requiring external dependencies. Import them and use their exported functions directly on your data structures.

The power tools for maps and slices

You are building a configuration loader. You have a map of settings loaded from a file. You need to check if a specific key exists before accessing it. You write val, ok := m[key]; if ok { ... }. It works. Now you need to copy that map so you can apply user overrides without mutating the shared default configuration. You reach for a for loop to iterate over keys and copy them one by one. It is tedious. You have written this loop five times in the last month. You also need to sort a slice of timestamps and filter out expired entries. You write another loop.

Go 1.21 introduced the maps and slices packages to the standard library to handle these common patterns. These packages provide generic functions for cloning, comparing, sorting, and searching maps and slices. You get consistent, optimized implementations without pulling in external dependencies.

Think of Go's built-in syntax as the raw materials. You can build anything with loops and conditionals. The maps and slices packages are the power tools. You can drill a hole with a hammer and a nail, but a drill makes the job faster, cleaner, and less error-prone. These packages use generics, so the compiler generates specialized code for your types. There is no runtime overhead compared to a hand-written loop.

Core operations

Here is the baseline usage: clone a map, check a key, sort a slice, and check for a value.

package main

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

func main() {
	// Create a map to work with.
	// Maps are unordered; iteration order is not guaranteed.
	config := map[string]int{"timeout": 30, "retries": 3}

	// Clone creates a new map with the same keys and values.
	// This is a shallow copy; pointer values are shared.
	safeConfig := maps.Clone(config)
	safeConfig["timeout"] = 60

	// Has checks for key existence without the comma-ok idiom.
	// Returns true if the key is present, false otherwise.
	if maps.Has(config, "retries") {
		fmt.Println("Retries key exists")
	}

	// Sort rearranges the slice in place using the default order.
	// For ints, this means ascending numerical order.
	priorities := []int{3, 1, 4, 1, 5}
	slices.Sort(priorities)

	// Contains returns true if the value appears in the slice.
	// Useful for quick membership checks without a loop.
	if slices.Contains(priorities, 4) {
		fmt.Println("Priority 4 is present")
	}
}

How the operations work

maps.Clone allocates a new map and copies every key-value pair from the source. The copy is shallow. If your map values are pointers, slices, or structs containing pointers, the clone shares those inner references. Modifying the pointee through the clone affects the original map. Use maps.Clone when your values are immutable primitives or when you intend to mutate the map structure without touching the underlying data.

maps.Has checks for a key and returns a boolean. It avoids the verbose comma-ok idiom when you only care about existence. The comma-ok idiom is still useful when you need the value and the existence check in one step. maps.Has shines when you are validating keys before processing or when you want cleaner control flow.

maps.Equal compares two maps for equality. It checks that both maps have the same keys and that corresponding values are equal. This is faster and safer than reflect.DeepEqual, which can traverse cycles and compare unexported fields unexpectedly. The community prefers maps.Equal for map comparison because it avoids the overhead and complexity of reflection.

slices.Sort rearranges the slice in place. It uses a sorting algorithm optimized for performance. The type must be ordered, meaning the elements must support the < operator. You can sort slices of int, string, or float64 directly. If you need to sort a slice of structs, you must provide a custom less function using slices.SortFunc.

slices.Contains scans the slice linearly and returns true if it finds the value. This is an O(n) operation. It is efficient for small slices or one-off checks. Do not use slices.Contains inside a tight loop over a large collection. Build a map for O(1) lookups if you need to check membership repeatedly.

Realistic usage patterns

Here is a function that merges configuration updates while protecting the original state. It demonstrates cloning, merging, and validation.

// ProcessRequest handles an incoming configuration update.
// It merges new values and validates required keys.
func ProcessRequest(base map[string]string, updates map[string]string) map[string]string {
	// Clone the base map so we don't mutate shared state.
	// This ensures the original base map remains untouched.
	result := maps.Clone(base)

	// Merge updates into the clone.
	// Values in updates override matching keys in result.
	maps.Merge(result, updates)

	// Check for required keys using Has.
	// This avoids the verbose comma-ok pattern for simple checks.
	if !maps.Has(result, "api_key") {
		panic("missing api_key")
	}

	return result
}

Here is how to filter a slice by removing elements that match a condition. slices.DeleteFunc is a powerful tool for in-place filtering.

// FilterActiveUsers removes inactive users from the list.
// It modifies the slice in place and returns the new length.
func FilterActiveUsers(users []User) []User {
	// DeleteFunc removes elements where the predicate returns true.
	// It shifts remaining elements to fill the gap and reslices.
	// The slice capacity is preserved to avoid reallocation.
	return slices.DeleteFunc(users, func(u User) bool {
		return !u.IsActive
	})
}

Pitfalls and compiler errors

maps.Clone performs a shallow copy. If you clone a map of []byte or *struct, the clone shares the underlying data. Modifying the byte slice through the clone corrupts the original. Deep copying requires manual iteration or a library like github.com/mohae/deepcopy for complex structures.

slices.Sort requires an ordered type. If you try to sort a slice of structs, the compiler rejects the program with invalid operation: cannot compare a < b (operator < not defined on struct). Use slices.SortFunc with a custom comparison function to sort structs.

// SortFunc allows custom ordering for types that are not naturally ordered.
// The less function defines the sort order for two elements.
slices.SortFunc(items, func(a, b Item) int {
	// Return negative if a < b, positive if a > b, zero if equal.
	return cmp.Compare(a.Score, b.Score)
})

slices.Contains is O(n). Using it in a nested loop creates O(n²) complexity. If you have a large list of IDs and you need to check membership for every item in another list, build a map first. The compiler will not warn you about performance issues. You must reason about the algorithm.

Goroutine leaks are unrelated to these packages, but remember that maps and slices functions do not handle concurrency. If multiple goroutines access the same map or slice, you must synchronize access with a mutex. These packages are not thread-safe.

Cloning is shallow. Know your data.

Decision matrix

Use maps.Clone when you need a copy of a map and the values are immutable or shallow copy is sufficient. Use maps.Has when you only care about key existence and want to avoid the comma-ok idiom. Use maps.Equal when comparing two maps for equality and want to avoid reflection. Use maps.Merge when combining multiple maps where later values should override earlier ones.

Use slices.Sort when you need to order a slice of comparable types like ints or strings. Use slices.SortFunc when sorting a slice of structs or when you need a custom ordering criterion. Use slices.Contains when checking for a value in a small slice or a one-off check. Use slices.DeleteFunc when filtering a slice by removing elements that match a predicate. Use slices.Clone when you need a copy of a slice and want clearer code than append(nil, s...).

Use a manual loop when you need to transform elements, perform complex logic during iteration, or break early based on conditions. Use a map for lookups when you have a large collection and need O(1) performance for membership tests. Use reflect.DeepEqual only when you must compare values with unexported fields or when dealing with interfaces that hide the underlying type.

Sort in place. Don't expect a new slice.

Standard library wins. Don't import a third-party package for basic slice operations.

Where to go next