31.8 Constraints Defined in golang.org/x/exp/constraints

Alright, let’s talk about the golang.org/x/exp/constraints package. This is where the Go team put all the shiny, useful constraint toys for us to play with when generics landed. Think of it as the official, but slightly experimental, toolbox for describing what kinds of types your generic functions can accept. First, a crucial reality check: this package lives in exp, which is Go-speak for “experimental.” This means the Go team reserves the right to change their minds, break your code, and move things into the main standard library whenever they feel like it. It’s incredibly useful, but you don’t want to bet your company’s core infrastructure on it without a clear exit strategy. Most of these constraints are so fundamental that they’ll likely be stabilized somewhere, but just be aware you’re living on the edge.

31.7 Generic Data Structures: Stack, Queue, and Set

Alright, let’s get our hands dirty. Generic data structures are where this whole “type parameter” thing stops being an academic exercise and starts paying rent. You’ve probably hand-rolled a Stack of ints or a Queue of SomeStruct a dozen times. It’s boring, it’s error-prone, and it’s exactly the kind of mind-numbing repetition generics are meant to erase. We’re going to build three classics: a Stack, a Queue, and a Set. But we’re going to build them once, for any type (well, any comparable type, in the Set’s case). You’ll see where the power lies, and also where Go’s designers, in their infinite wisdom, decided to put some frustrating little speed bumps.

31.6 Instantiation: How the Compiler Generates Code

Alright, let’s pull back the curtain on the real magic: instantiation. This is the moment where our generic function blueprint gets stamped out into a real, concrete, type-specific function that your CPU can actually execute. The compiler isn’t generating code at runtime; it’s doing all the heavy lifting right there while it compiles your program. Think of it less like a factory and more like a master baker who prepares all the ingredients before the party starts.

31.5 Type Sets: Union Constraints with ~

Alright, let’s talk about the tilde (~). This little squiggle is one of the most elegant and simultaneously confusing additions to Go’s type system. It exists to solve a very specific, very real problem that emerges the moment you start writing generic functions with constraints like int. Imagine you write this perfectly reasonable function: func PrintInts[T int](values []T) { for _, v := range values { fmt.Println(v) } } You try to use it, and immediately hit a wall.

31.4 comparable: The Constraint for == and !=

Alright, let’s talk about comparable. You’ve probably been banging your head against the wall, wondering why you can’t just write a generic function to check if two things are equal. You try this: func Equal[T any](a, b T) bool { return a == b } And the compiler slaps you down immediately. “Invalid operation: a == b (operator == not defined on T)”. Rude. The reason is simple and, honestly, sensible: Go is a brutally pragmatic language. The == operator doesn’t work for every single type you could possibly shove into a type parameter. What does == mean for a map? Or a function? Or a slice? It’s undefined, so Go just says “nope” rather than letting you stumble into a runtime panic.

31.3 Constraints: Limiting What T Can Be

Right, so you’ve got your shiny new type parameter T and you’re ready to write some beautifully generic code. You try to write a function that sums a slice of numbers, func Sum[T any](s []T) T, and immediately you hit a wall. You can’t use the + operator. Why? Because any means literally any type. You could be summing []int, []string, or []http.Request—and Go’s compiler, being the stubbornly pragmatic friend that it is, refuses to let you add two http.Requests together. This is the problem constraints are designed to solve.

31.2 Type Parameters: func F[T any](x T) T

Alright, let’s get our hands dirty. You’ve probably written a dozen functions that do the exact same thing, just for different types. You copy, paste, change int to string, and die a little inside. We’ve all been there. Generics in Go, specifically the T any part you see here, are our long-awaited pardon from that particular flavor of tedium. The syntax func F[T any](x T) T might look a bit alien at first, but break it down. Before the function name F, we declare our type parameters in square brackets. [T any] is the simplest form: we’re saying “For this function, we’re going to use a type we’ll call T. The any part is its constraint, meaning T can literally be… any type.” It’s the equivalent of a wildcard. Inside the function body, x is of that type T, and the function also returns a value of type T.

31.1 The Problem Before Generics: Code Duplication and interface{}

Right, let’s talk about the bad old days. You know, the ones we’re all pretending to forget now that we have generics. Before Go 1.18, if you wanted to write a function that could handle multiple types, you were faced with a classic engineering trade-off: do the wrong thing fast, or do the wrong thing slowly. Your two main options were code duplication or the infamous interface{}. Let’s say you wanted a simple function to find the maximum value in a slice. A simple task, right? Not if you needed it for int, float64, and string slices. Your first, most visceral reaction was to just write the same function three times.

— joke —

...