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
172: Generic Functions Revisited
A few years ago, I was reviewing a PR from a colleague who was building a high-performance caching layer. He had written a beautiful generic function to handle cache invalidation, using a type constraint like comparable for the keys. It worked perfectly for string and int. But the moment he introduced a custom type—type RequestID string—the compiler started screaming at him in a few specific edge cases involving interface assertions. He spent three hours questioning if he'd fundamentally misunderstood generics, only to realize he was fighting against the difference between a type and its underlying type.
When we first encounter generics in Go, we tend to stick to the basics: [T any] or [T comparable]. That gets you through 80% of your needs. But as you start building real-world libraries or complex internal frameworks, you'll find that those broad constraints are often too blunt. You'll realize that Go's type system is stricter than you remember, and you'll need to revisit how you define what T actually is.
The Magic of Type Approximation
The most common "gotcha" when revisiting generic functions is the difference between string and ~string. If you define a constraint as interface { string }, it only accepts the literal string type. If you have a type Email string, that type does not satisfy the constraint, even though it's just a string under the hood.
This is where the tilde (~) comes in. By using ~string, you are telling Go: "I don't care if this is exactly a string, as long as its underlying type is a string." I've seen this trip up even senior devs because it's a subtle distinction that only matters when you start creating domain-specific types.
type Numeric interface {
~int | ~float64
}
func Sum[T Numeric](nums []T) T {
var total T
for _, v := range nums {
total += v
}
return total
}
type Price int // Custom type
func main() {
prices := []Price{10, 20, 30}
// This only works because of the ~ in the Numeric interface
fmt.Println(Sum(prices))
}
Composition and Custom Constraints
Another area where we often need to refine our approach is when comparable isn't enough. comparable is great for using a type as a map key, but it doesn't help you if you need to perform a specific action on that type—like comparing two versions of a config file or checking if a user object is "equal" based on an ID rather than every single field.
The professional way to handle this is by composing constraints. Instead of relying on built-in constraints, you create an interface that combines a type set (the ~type part) with a method set. This allows your generic function to remain flexible while still guaranteeing that the type T has the behavior you need.
I usually recommend this pattern when you find yourself writing any and then using a type switch inside your function. If you're type-switching inside a generic function, you're usually defeating the purpose of generics. Instead, move that requirement into the constraint.
type Identifiable interface {
comparable
ID() string
}
func FindDuplicate[T Identifiable](items []T) []T {
seen := make(map[string]T)
var dups []T
for _, item := range items {
id := item.ID()
if _, exists := seen[id]; exists {
dups = append(dups, item)
}
seen[id] = item
}
return dups
}
Notice how Identifiable requires the type to be comparable AND have an ID() method. This is a powerful pattern because it keeps your logic clean and pushes the requirement onto the type definition, rather than the function implementation.
📋 Practical Task
Implement a Generic "UniqueBy" Filter
In this exercise, you will build a utility function that filters a slice of any type, removing duplicates based on a specific property of that type, rather than the whole object.
Requirements:
- Define a constraint interface called
Keyablethat requires a methodGetKey() string. - Implement a generic function
UniqueBy[T Keyable](input []T) []T. - The function should return a new slice containing only the first occurrence of each element based on the value returned by
GetKey(). - Create a custom type
Userwith fieldsID intandName string. Implement theGetKey()method forUserto return the ID as a string. - Test your function with a slice of
Userstructs that contains multiple users with the same ID but different names (simulating stale data).
There are no comments for now.