JavaRush /Courses /Go SELF /Packages slices, maps, cmp

Packages slices, maps, cmp

Go SELF
Level 27, Lesson 2
Available

1. Why use slices/maps/cmp and where to find them

If you’re just starting out, it’s very easy to fall into a trap: “But I can write for, why do I need any packages?” And it’s true—the for loop in Go is universal. But the problem isn’t that we can’t write a loop. The problem is that a hand-written loop almost always hides intent.

When you see slices.Delete(tasks, i, j), your brain reads it as: “delete a range.” When you see tasks = append(tasks[:i], tasks[j:]...), your brain reads it as: “uhh… wait… are those bounds correct?..”.

Also, the standard library is a collection of solutions that have already gone through “a thousand rakes,” including subtle things like preserving the type for named slices and behaving in a less surprising way for the garbage collector. The slices package was designed specifically as a set of generic helpers for slices of any type.

So we’re not just talking in the abstract—we’ll rely on a small learning app: an in-memory mini task tracker. We have a Task, a slice of tasks, and sometimes a map index for quick access.

package main

import "fmt"

type Task struct {
	ID       int
	Title    string
	Priority int
	Done     bool
}

func main() {
	tasks := []Task{
		{ID: 1, Title: "Buy milk", Priority: 2},
		{ID: 2, Title: "Learn Go", Priority: 1},
	}
	fmt.Println(tasks) // [{1 Buy milk 2 false} {2 Learn Go 1 false}]
}

A mini map of the packages and their relationship to generics

When you see slices.SortFunc or maps.Clone, you might want to ask: “why aren’t these methods?” or “why isn’t this in sort?”—and those are totally fair questions. The answer is simple: these packages appeared as a convenient layer over common operations, but already in the generics era. In the Go 1.21 release, the addition of slices, maps, and cmp to the standard library was highlighted.

It helps a lot to keep an “intent map” in your head:

Package What it’s about Typical feeling in code
slices
operations on []T “I’m doing the same thing I used to do with a loop, but shorter and more readable”
maps
operations on map[K]V “I copy/compare/get keys without a manual for range”
cmp
comparison of orderable values “I define ordering using standard comparisons; less home-grown routine code”

There’s also a bit of “magic” that’s important when reading other people’s code: in slices and maps signatures you’ll often see S ~[]E or M ~map[K]V. This is done so functions work not only with plain []T, but also with named slice/map types (for example, type TaskList []Task). That makes signatures look “longer than you’d like,” but it results in a more practical API.

2. slices: common operations without “index acrobatics”

The slices package is basically “the most common things you do to a slice by hand, but with a readable button.” This is especially valuable in learning projects and real production code for one reason: slice boundary operations are the fastest way to catch a bug of the “why does it sometimes work?” kind. And the second bonus: slices expresses intent, and intent is half of maintainability.

slices.Clone: copying as an “ownership boundary”

Sometimes we want to take a task list and sort it “for display,” but not change the original order (for example, in memory we keep the insertion order, but we want a nice output). That’s where Clone is a clear signal: “I’m making a copy and I’ll mutate it however I want from here on.”

package main

import (
	"fmt"
	"slices"
)

func main() {
	tasks := []int{3, 1, 2}
	cp := slices.Clone(tasks)

	cp[0] = 999
	fmt.Println(tasks) // [3 1 2]
	fmt.Println(cp)    // [999 1 2]
}

The standard library signature of Clone is designed to preserve the type for named slices, rather than “dropping” everything back to []T. This is exactly the case where the standard solution is more careful than the home-grown “copy via append([]T(nil), s...)”.

slices.Sort and slices.SortFunc: in-place sorting

Sorting is a perfect example of the difference between “a function mutates” and “a function returns.” slices.Sort and slices.SortFunc sort in place, meaning they reorder the existing slice and return nothing. That’s convenient: the code makes it clear you’re not expecting a new slice.

Let’s say we want to sort tasks by ID (just as an example). For built-in ordered types there is slices.Sort, but for structs you need SortFunc.

package main

import (
	"fmt"
	"slices"
)

type Task struct {
	ID    int
	Title string
}

func main() {
	tasks := []Task{{ID: 2, Title: "B"}, {ID: 1, Title: "A"}}

	slices.SortFunc(tasks, func(a, b Task) int {
		if a.ID < b.ID {
			return -1
		}
		if a.ID > b.ID {
			return 1
		}
		return 0
	})

	fmt.Println(tasks) // [{1 A} {2 B}]
}

Yes, the comparator looks a bit verbose. But that’s the honest price for the fact that ordering for structs is almost always domain-specific (we decide what matters more: priority, date, ID, title, etc.).

slices.Delete: deleting a range is safer than hand-written append

Deleting from the middle of a slice is a classic. Before slices.Delete, everyone wrote variations of append(s[:i], s[j:]...). It works, but it looks like a spell: get one rune wrong and you summon the off-by-one demon.

Delete states the intent directly: delete the [i, j) range (just like a normal slice expression s[i:j], the right bound is exclusive).

package main

import (
	"fmt"
	"slices"
)

func main() {
	ids := []int{10, 20, 30, 40}
	ids = slices.Delete(ids, 1, 3) // delete 20 and 30
	fmt.Println(ids)               // [10 40]
}

Pay attention to an important “mechanical” detail: Delete returns a new slice (more precisely, a new slice header), so you almost always need an assignment ids = …. It’s a close relative of append: if the length changes, you return a value.

3. maps: cloning, comparing, and getting keys without manual range

The maps package does for map[K]V roughly what slices does for []T: it removes routine code where you write the same loop every time. It’s not a “mandatory package,” but it works great as an intent marker: you show the reader that you’re cloning, comparing, or moving data, without distracting them with mechanics.

It’s important to remember: maps does not change the key property of map—iteration order is still not guaranteed. That’s why it’s convenient to combine maps with slices and cmp when you need stable output.

maps.Clone: a “snapshot” for safe work

Suppose we want to keep a task index by ID:

package main

import (
	"fmt"
	"maps"
)

func main() {
	byID := map[int]string{1: "Buy milk", 2: "Learn Go"}
	snapshot := maps.Clone(byID)

	snapshot[1] = "Hacked"
	fmt.Println(byID[1])     // Buy milk
	fmt.Println(snapshot[1]) // Hacked
}

This is useful when you want to take a “point-in-time snapshot” and analyze it without worrying that the original will change. At the same time, remember an important caveat: cloning a map copies key/value pairs, not a “deep copy” of all nested structures (if values are reference-like, the reference is copied). That’s not a maps issue—it’s the nature of values in Go.

maps.Copy: moving data into an existing map

Sometimes you don’t want to create a new map—you want to extend an existing one. Then Copy looks simpler than a loop.

package main

import (
	"fmt"
	"maps"
)

func main() {
	dst := map[int]string{1: "A"}
	src := map[int]string{2: "B", 3: "C"}

	maps.Copy(dst, src)
	fmt.Println(dst) // map[1:A 2:B 3:C]
}

maps.Equal: expressive map comparison

Comparing maps by hand is another eternal loop: “check length, check all keys, compare values…”. If the values are comparable, maps.Equal solves it concisely and expressively.

package main

import (
	"fmt"
	"maps"
)

func main() {
	a := map[string]int{"x": 1, "y": 2}
	b := map[string]int{"y": 2, "x": 1}

	fmt.Println(maps.Equal(a, b)) // true
}

4. cmp, stable output, and the moment when a loop is more honest

cmp: less boilerplate in comparators for basic types

The cmp package is small but very pleasant, especially when you want to sort or compare values and you want less boilerplate. Its goal is to provide standard comparison functions for ordered types (for example, int, string).

The most practical scenario: you sort a []int or []string via slices.SortFunc, but you don’t want to write the comparator yourself.

package main

import (
	"cmp"
	"fmt"
	"slices"
)

func main() {
	nums := []int{3, 1, 2}
	slices.SortFunc(nums, cmp.Compare[int])

	fmt.Println(nums) // [1 2 3]
}

What’s nice here is that cmp.Compare[int] is already a ready-made function of the required shape func(a, b int) int. So cmp doesn’t replace slices—it pairs well with it.

If we sort tasks by Priority, we can make the comparator a bit cleaner: compare priority first, and if equal—compare title. For the second part (strings) we can also use cmp.Compare[string].

package main

import (
	"cmp"
	"fmt"
	"slices"
)

type Task struct {
	Title    string
	Priority int
}

func main() {
	tasks := []Task{{"B", 2}, {"A", 2}, {"C", 1}}

	slices.SortFunc(tasks, func(a, b Task) int {
		if p := cmp.Compare(a.Priority, b.Priority); p != 0 {
			return p
		}
		return cmp.Compare(a.Title, b.Title)
	})

	fmt.Println(tasks) // [{C 1} {A 2} {B 2}]
}

maps + slices + cmp: stable output on top of a map

Very often, applications need to print data from a map for a person (or into logs), and you suddenly discover that the order “jumps around.” This isn’t a Go bug—it’s the map contract: iteration order is not guaranteed. So the typical technique is: extract keys, sort keys, then print values by the sorted keys.

Below is a small combo where we have a map[int]Task and we produce stable output by ID.

package main

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

type Task struct {
	Title string
}

func main() {
	byID := map[int]Task{2: {"Learn Go"}, 1: {"Buy milk"}}

	ids := maps.Keys(byID)
	slices.SortFunc(ids, cmp.Compare[int])

	for _, id := range ids {
		fmt.Println(id, byID[id].Title)
		// 1 Buy milk
		// 2 Learn Go
	}
}

And here it’s especially clear why this is useful: the code speaks for itself. We don’t argue with map; we just build stable output on top of it.

When the standard solution is better, and when a loop is more readable

It’s tempting to decide that now “proper Go” means writing everything through slices and maps. In reality, proper Go is code that’s easy to read and hard to break.

Sometimes slices.Delete really is better than append with three slices. And sometimes a plain loop is better than a function with a comparator because the logic is too domain-specific and you need to do something non-standard step by step.

A simple “rule of thumb” is: if you’re doing a typical operation (copy, delete a range, sort, compare, check membership) and the standard function expresses the intent directly—use the standard one. If the operation is unique, with many conditions and custom behavior (for example, “delete tasks only if they’re done and older than N, but keep at least one ‘latest done’ for reporting”)—a loop is more honest because it shows the details.

It’s also useful to remember one more thing: these standard packages were written not to replace thinking, but to replace routine. Routine is best delegated to the standard library. Thinking—we keep for ourselves (it still doesn’t delegate that easily, even for money).

5. Common mistakes when using slices/maps/cmp

Mistake #1: forgot the assignment after slices.Delete (or a similar function that returns a slice).
This happens especially often to beginners, because visually it feels like: “I deleted it.” But deletion changes the slice length, which means the function returns an updated slice “header.” If you don’t do tasks = slices.Delete(tasks, i, j), you’ll just throw away the result and keep living with the old length.

Mistake #2: expecting slices.Sort to return a new slice.
Sorting reorders elements in place and returns nothing. Because of this, people sometimes create extra variables or try to assign the result: tasks = slices.Sort(tasks)—that won’t work. The right mental model is: “Sort mutates, Delete returns.”

Mistake #3: mixed up the [i, j) range and assumed j is inclusive.
Delete(s, i, j) deletes elements with indices i, i+1, …, j-1. This is exactly the same logic as the slice expression s[i:j]. The off-by-one mistake here is especially annoying: the code compiles, but the result will be silently wrong.

Mistake #4: trying to use cmp.Compare where the type is not ordered.
cmp.Compare is great for int, string, and other ordered types. But if you’re sorting structs where ordering is defined by fields, you need your own comparator. This isn’t a drawback of cmp; it’s the normal price of the fact that ordering for domain entities is almost always “ours,” not built-in.

Mistake #5: expecting “deep copying” from maps.Clone and slices.Clone.
Both functions copy the collection structure (slice elements or map pairs). But if pointers, slices, or maps are inside, the references to those objects are copied. Sometimes that’s fine, sometimes it isn’t—but it’s important not to be surprised by it in the middle of debugging at 2 a.m.

1
Task
Go SELF, level 27, lesson 2
Locked
Sorting a list
Sorting a list
1
Task
Go SELF, level 27, lesson 2
Locked
Two types of order
Two types of order
1
Task
Go SELF, level 27, lesson 2
Locked
Cut out a fragment
Cut out a fragment
1
Task
Go SELF, level 27, lesson 2
Locked
Settings snapshot
Settings snapshot
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION