JavaRush /Courses /Go SELF /Generics and the standard library

Generics and the standard library

Go SELF
Level 27, Lesson 4
Available

1. Context: readability and a small task list

When you first see slices.SortFunc(xs, cmp.Compare[int]), your brain sometimes reacts as if you opened a portal into a world of “big scary words.” In reality, it’s the opposite: the standard library tries to pack typical, boring, repetitive loops into short calls with obvious intent. The idea is not to hide logic, but to remove noise.

In Go, code that reads “left to right” like a story is highly valued: what we want to do, not which exact loop we’re about to write for the 27th time in our life. That’s why slices.Delete expresses “delete a range,” not “do acrobatics with append and slicing.” It’s also important to understand the mechanics: if a function changes a slice’s length, it almost always returns a new slice (like append), and if it only changes the order of elements, it often works in place and returns nothing (like sorting).

One more useful guideline: the language and the standard library gradually “align” phrasing and APIs so that common code reads more simply. This is noticeable in how the generics ecosystem in the stdlib has evolved: many everyday operations can now be expressed with standard calls, without hand-rolled loops and reinvented wheels.

Mini example: a task list as a playground for slices/maps/cmp

To avoid speaking abstractly, we’ll continue our teaching style: small code examples that assemble into one mini app. Let it be a simple in-memory task list: tasks with ID, Title, Done, and tags. We won’t touch files, network, or time—just pure data processing, because that’s where slices/maps/cmp really shine.

Let’s start with the model and a couple of stubs. We’ll immediately make Tasks a separate type based on a slice—this is convenient because you can add methods, and the code starts to “read itself” (instead of being a set of scattered functions).


package main

type Task struct {
	ID    int
	Title string
	Done  bool
	Tags  []string
}

type Tasks []Task

To keep the examples lively later, let’s add “demo data” via a function. Yes, it’s not a database. Yes, it’s not prod. But your brain won’t get distracted.

package main

func sampleTasks() Tasks {
	return Tasks{
		{ID: 3, Title: "buy milk", Done: true, Tags: []string{"home"}},
		{ID: 1, Title: "write a report", Done: false, Tags: []string{"work"}},
		{ID: 2, Title: "read about slices", Done: false, Tags: []string{"study", "go"}},
	}
}

slices + cmp: sorting without hand-written “if/else ladders”

Sorting is one of the most common places where beginners start writing “complicated code for a simple result.” Usually it looks like a manual sort.Slice, then inside: compare fields, then another comparison, then another… and five minutes later you’ve already forgotten what you were sorting. The slices + cmp combo lets you make sorting both shorter and more readable: you explicitly show what you sort by, and you don’t drown in details.

The idea of cmp is simple: a comparator returns -1/0/1, meaning “less / equal / greater.” This kind of comparator is convenient to compose and reuse, especially in slices.SortFunc. And most importantly, the code stops looking like a riddle.

The simplest warm-up: sorting numbers “like a human”

Let’s start with a short example with no structs—just to get used to it visually.

package main

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

func main() {
	nums := []int{10, 3, 7}
	slices.SortFunc(nums, cmp.Compare[int])
	fmt.Println(nums) // [3 7 10]
}

Here it’s important to feel the idea: cmp.Compare[int] is a ready-made comparison function for int that you can “pass as a parameter.” As a result, sorting reads almost like a sentence: “sort nums using int comparison.”

Sorting tasks by ID: short, without losing meaning

Now we apply the same approach to our task list. Let’s write a method that sorts by ID. And let’s immediately do an important thing: we won’t mutate the original slice; we’ll make a copy. This is a good habit because sorting changes element order in place.

package main

import (
	"cmp"
	"slices"
)

func (ts Tasks) SortedByID() Tasks {
	cp := slices.Clone(ts)
	slices.SortFunc(cp, func(a, b Task) int {
		return cmp.Compare(a.ID, b.ID)
	})
	return cp
}

Notice: slices.Clone here directly expresses the intent “make a separate copy of the slice.” It’s a standard function for slices, like maps.Clone for maps, and it helps you avoid a “surprise” from accidental data mutation.

Sorting by multiple keys: Done → Title → ID

Now for the good part: multi-level sorting usually turns into an “if ladder.” With cmp, you can do it neatly: compare one key; if it’s not equal, return; if it is equal, compare the next.

package main

import (
	"cmp"
	"slices"
)

func (ts Tasks) SortedForOutput() Tasks {
	cp := slices.Clone(ts)

	slices.SortFunc(cp, func(a, b Task) int {
		// false < true, so unfinished (false) will come first
		if r := cmp.Compare(a.Done, b.Done); r != 0 {
			return r
		}
		if r := cmp.Compare(a.Title, b.Title); r != 0 {
			return r
		}
		return cmp.Compare(a.ID, b.ID)
	})

	return cp
}

Yes, you can compare bool via cmp.Compare, too. And this is one of those moments where a “standard comparator” makes the code clearer: you have a crisp comparison order, and it’s easy to swap keys around without breaking your brain.

maps + slices: stable output and clear operations on map[K]V

Maps in Go are great, but they have one “personality trait”: iteration order via range is not guaranteed. So any output of stats or grouping via a map can come out “dancing”—one way today, another tomorrow. For humans it’s annoying; for tests and scripts it’s a disaster. So you almost always need stable output: collect keys into a slice, sort it, then print.

The maps package helps you write less manual code when you need to clone, copy, or compare maps. And slices helps bring keys into a stable order. It’s like two hands of one organism: one gathers data, the other makes output predictable.

Tag counters: building map[string]int

Let’s count how many times each tag appears across all tasks. This is a typical problem: “walk the data and grow counters.”

package main

func (ts Tasks) TagStats() map[string]int {
	stats := make(map[string]int)

	for _, t := range ts {
		for _, tag := range t.Tags {
			stats[tag]++
		}
	}
	return stats
}

So far, everything is ordinary. The magic starts at output.

Stable output: keys → sorting → printing

Stable output means you choose the order yourself. We’ll do this: take the keys, sort them, then print.

package main

import (
	"fmt"
	"slices"
)

func printTagStats(stats map[string]int) {
	keys := make([]string, 0, len(stats))
	for k := range stats {
		keys = append(keys, k)
	}
	slices.Sort(keys)

	for _, k := range keys {
		fmt.Printf("%s: %d\n", k, stats[k])
	}
}

There’s no maps.Keys here so we don’t get stuck on version details and lose focus: even this short loop reads fine. The more important point is this: slices.Sort(keys) shows the intent “sort strings,” and that reads simpler than manual work with the sort package (though it’s fine, too).

maps.Clone and a “snapshot”: when it’s useful

Sometimes you need to take a “snapshot” of stats before changing them. For example, you compute stats, then want to “fix” them (add an artificial tag, merge two tags), while keeping the original for comparison or debugging. Then maps.Clone is a simple, readable step.

package main

import "maps"

func copyStats(stats map[string]int) map[string]int {
	return maps.Clone(stats)
}

The nuance to remember: maps.Clone makes a shallow copy. If the value is a number or a string, it’s perfect. If the value is a slice or a struct with references, it copies references, not the “entire universe.” That’s not a bug; it’s an honest contract.

4. Small generic helpers that complement slices/maps/cmp

It’s very easy to slide into “let’s write our own mini-stdlib”: Map, Filter, Reduce, FlatMap, Distinct, and in a day your project will look like a suspicious parody of functional languages. But there’s another path: write 1–2 small generic helpers that actually cover a recurring pattern, and then combine them with standard slices/maps/cmp.

The key principle: if the standard library already expresses the intent, use it. If you have the same loop repeated in 3–4 places and you’re afraid of making a mistake every time, carefully extract it into a generic function. That’s the “golden middle,” where generics help without taking over the project.

Filter[T]: filtering a slice without losing the type

Filtering is a super-common operation. Yes, you can write a loop every time. But Filter as a utility usually ends up short and clear. We’ll make a version that returns a new slice and tries to be memory-friendly (preallocates capacity).

package main

func Filter[T any](xs []T, keep func(T) bool) []T {
	out := make([]T, 0, len(xs))
	for _, x := range xs {
		if keep(x) {
			out = append(out, x)
		}
	}
	return out
}

Now you can filter tasks by completion status—and the Task type is preserved, unlike the “universal” approach via []any.

package main

func (ts Tasks) Undone() Tasks {
	return Filter(ts, func(t Task) bool {
		return !t.Done
	})
}

GroupBy[K comparable, T any]: grouping into map[K][]T

Grouping is the second typical pattern. And it fits generics very well: the key must be comparable (because it’s a map key), and the element can be anything.

package main

func GroupBy[T any, K comparable](xs []T, key func(T) K) map[K][]T {
	groups := make(map[K][]T)
	for _, x := range xs {
		k := key(x)
		groups[k] = append(groups[k], x)
	}
	return groups
}

For example, let’s group tasks by the Done status:

package main

func (ts Tasks) GroupByDone() map[bool]Tasks {
	raw := GroupBy(ts, func(t Task) bool { return t.Done })

	// a small “polish” to return map[bool]Tasks, not map[bool][]Task
	out := make(map[bool]Tasks, len(raw))
	for k, v := range raw {
		out[k] = Tasks(v)
	}
	return out
}

Yes, there’s a small loop here to “polish up” the type. It’s not scary: in return, it’s convenient to keep working with Tasks as a “slice with methods.”

5. Putting it all together: a short main that reads like a script

The final goal of this lecture is for the code at the call site to read as simple steps. Not “we’re doing a clever trick here,” but “we take tasks, filter, sort, build stats, print.” When main becomes such a “tape of actions,” the project becomes easier to maintain: you quickly find where what happens, and you’re less afraid to change code.

Now we’ll assemble a demo main that uses our Tasks type, the package functions slices/maps/cmp, and a couple of small generic helpers. No magic—just composition, like LEGO, but without losing parts under the couch.

Printing tasks in a stable order

Let’s make a small print function so the examples are complete. Printing isn’t the goal of the lecture, but it lets you see the result.

package main

import "fmt"

func printTasks(ts Tasks) {
	for _, t := range ts {
		fmt.Printf("#%d done=%v title=%q tags=%v\n", t.ID, t.Done, t.Title, t.Tags)
	}
}

Final main: minimal code, maximum intent

Now the main scenario.

package main

import "fmt"

func main() {
	tasks := sampleTasks()

	fmt.Println("All (sorted for output):")
	printTasks(tasks.SortedForOutput())

	fmt.Println("\nUndone only (sorted by id):")
	printTasks(tasks.Undone().SortedByID())

	fmt.Println("\nTag stats:")
	printTagStats(tasks.TagStats())
}

This main matters not for its output, but for how it reads: each expression looks like a “sentence.” That’s exactly why we combine standard generics packages and a couple of small in-house utilities.

6. Common mistakes

Mistake #1: expecting sorting to return a new slice.
This is a common trap from the world of “functional thinking”: it feels like any operation should return a result. In Go, sorting usually rearranges elements in place, meaning it mutates the same slice. So if you want to keep the original order, make a copy first via slices.Clone, and then sort the copy. The “copy → sort” model is especially important in code where the same list is used in different views.

Mistake #2: forgetting assignment after operations that change length.
There are functions (both in the standard library and your own) that change a slice’s length. By contract, such operations must return a new slice (like append or slices.Delete), because the length could have changed and the underlying array could even have been reallocated. If you call such a function and don’t assign the result back to a variable, your code “politely did nothing,” and you’ll be debugging emptiness.

Mistake #3: making Filter/GroupBy/Sort/Whatever “for every possible case” and turning the project into a library of abstractions.
Generics are very tempting: “oh, I can factor out everything.” But the cost is complex signatures, unclear names, and code that’s hard to debug for beginners (and for you a month later). A good practice: first use slices/maps/cmp where they directly express intent, and only then add 1–2 small generic helpers when you truly see a repeated pattern.

Mistake #4: unstable output from map and “random” results on screen.
Beginners often think: “I added keys in order A, B, C, so range over a map will also be A, B, C.” It won’t. Map iteration order is not guaranteed, and that’s fine. If you print something for the user or compare output in tests, stabilize the order: collect keys into a slice, sort with slices.Sort, then iterate over the keys. It’s almost always two extra lines that save hours of debugging.

Mistake #5: confusing “short” with “clear.”
Sometimes you can write one line that does everything at once. But if that line can’t be quickly explained to a teammate (or to yourself a week later), you didn’t win. The combination of generics and standard packages should shorten code by removing noise, not by turning logic into a puzzle. If you have to stare at an expression for a long time, it’s better to split it into a couple of variables with good names.

1
Task
Go SELF, level 27, lesson 4
Locked
Warehouse in Ascending Order
Warehouse in Ascending Order
1
Task
Go SELF, level 27, lesson 4
Locked
Frequencies in the report
Frequencies in the report
1
Task
Go SELF, level 27, lesson 4
Locked
Copy of tickets
Copy of tickets
1
Task
Go SELF, level 27, lesson 4
Locked
Grouped Cart
Grouped Cart
1
Survey/quiz
Generics, 27 level, 4 lesson
Unavailable
Generics
Generics
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION