Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
61: Generic Constraints and Interfaces
I've been thinking about how to make my utility libraries more reusable. I often find myself writing the same "Find Max" logic for int, then float64, and then some custom Price type. Naturally, I thought: "I'll just use generics and call it a day."
The "any" wall
My first instinct was to keep it as open as possible. I used any because, in my head, any type could potentially be compared. Let's look at what happened when I tried to compile this:
package main
import "fmt"
func Max[T any](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(10, 20))
}
The compiler immediately shut me down. It told me that the operator > is not defined for the type T. This makes sense if you think about it—any could be a struct, a map, or a channel. You can't use the "greater than" operator on a map. Go is being strict here to save us from a runtime panic.
The trap of "comparable"
Okay, so any is too broad. Go provides a built-in constraint called comparable. I figured, "This is exactly what I need." I swapped any for comparable and tried again:
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
Same error. I was confused for a second, but then I remembered that in Go, comparable specifically means the type supports == and !=. It does not mean it supports ordered comparisons like < or >. A struct is comparable if all its fields are comparable, but that doesn't mean one struct is "greater than" another.
Defining our own boundaries
Now we're getting to the real meat of generic constraints. If the language doesn't provide a built-in "Ordered" constraint, I can build my own using an interface. But I don't want a behavior-based interface (like "has a Compare method"); I want a type-set. I want to tell Go: "T can be any one of these specific types."
I'll define a Number constraint using the pipe | symbol, which acts as a union:
type Number interface {
int | int64 | float64
}
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
This works perfectly. By listing the types in the interface, I'm telling the compiler, "Trust me, whatever T ends up being, it's guaranteed to be one of these, and all of these support the > operator." It's a powerful pattern, though it can get verbose if you want to support every single numeric type (which is why the golang.org/x/exp/constraints package exists, but it's better to understand how it works under the hood first).
When behavior beats type sets
But wait—what if I have a custom type, like a Version struct that holds a major and minor number? I can't add a struct to a type-set union. The | operator only works with basic types.
This is where we pivot from "type constraints" to "behavior constraints." I'll define an interface that requires a specific method. If a type implements that method, it's allowed in my generic function.
type Comparable interface {
Compare(other Comparable) int // returns -1, 0, or 1
}
func MaxValue[T Comparable](a, b T) T {
if a.Compare(b) > 0 {
return a
}
return b
}
Notice the subtle shift. With the Number constraint, I was using language operators (>). With the Comparable constraint, I'm using a method call. You choose the constraint based on whether you need the type to be something (type set) or do something (method set). Using both in the same project is common; it just depends on whether you're dealing with primitives or complex objects.
📋 Practical Task
Build a Generic "Most Frequent Element" Finder
Create a function called MostFrequent[T comparable](slice []T) T. This function should take a slice of any comparable type and return the element that appears most often.
- Constraint: Use the
comparableconstraint because you will need to use the elements as keys in a map to track their frequency. - Requirement: The function should work equally well for a slice of strings
[]string{"apple", "banana", "apple"}and a slice of integers[]int{1, 2, 2, 3}. - Edge Case: If the slice is empty, you can either return the zero value of
Tor handle it via a secondboolreturn value (like(T, bool)).
There are no comments for now.