Go Generics: Your Magic Box That Works With Everything! 🎁
The Story of the Magic Box
Imagine you have a magical box. This box is special—it can hold anything you put inside it: toys, candies, books, or even your pet hamster! And the best part? The box always knows exactly what’s inside and gives you back the same type of thing you put in.
Before generics, Go programmers had to make separate boxes for each type of thing:
- A toy box (only for toys)
- A candy box (only for candies)
- A book box (only for books)
That’s a lot of boxes! 😅
Generics let you create one magic box that works with any type. Let’s learn how!
1. Type Parameters: Naming Your Magic Box’s Contents
What Are Type Parameters?
A type parameter is like putting a label on your magic box that says: “Whatever you put in here, I’ll remember it and give you back the same kind of thing.”
In Go, we write type parameters inside square brackets [T]. The T is just a placeholder name—like writing “STUFF” on your box.
Simple Example
// A magic box that holds ANY type
func GetFirst[T any](items []T) T {
return items[0]
}
Let’s break this down:
[T any]→ “T can be ANY type”items []T→ “Give me a list of T things”T(return) → “I’ll give back one T thing”
Using Your Magic Box
// Box holds numbers
nums := []int{10, 20, 30}
first := GetFirst(nums) // Returns 10
// Same box holds words!
words := []string{"hi", "bye"}
first := GetFirst(words) // Returns "hi"
One function. Works with everything! 🎉
The Rule of Names
You can name type parameters anything:
T→ Most common (Type)K→ Usually for KeysV→ Usually for ValuesE→ Usually for Elements
// Multiple type parameters
func Pair[K, V any](key K, val V) {
// K is one type, V is another
}
2. Type Constraints: Setting Rules for Your Box
The Problem With “Anything Goes”
Imagine your magic box can hold anything. But what if you want to add two things together?
You can add numbers: 5 + 3 = 8 ✅
But can you add a cat and a dog? 🐱 + 🐕 = ??? ❌
Type constraints are like saying: “This box only accepts things that can be added together!”
How Constraints Work
// Without constraint - can't add!
func Add[T any](a, b T) T {
return a + b // ERROR! Not all T can +
}
// With constraint - works!
func Add[T int | float64](a, b T) T {
return a + b // OK! Both can +
}
The | means “OR”:
int | float64= “only int OR float64 allowed”
Creating Your Own Constraint
// Define a rule: "Must be a number type"
type Number interface {
int | int32 | int64 | float32 | float64
}
// Use your rule
func Double[T Number](n T) T {
return n * 2
}
Now Double works with any number type, but rejects strings, structs, or other non-numbers.
Visual: How Constraints Filter Types
graph TD A["Any Type Wants In"] --> B{Passes Constraint?} B -->|int| C["✅ Allowed"] B -->|float64| C B -->|string| D["❌ Rejected"] B -->|struct| D
3. any and comparable: Built-in Magic Labels
Go gives you two pre-made constraints that are super useful!
The any Constraint
any means “I accept literally anything.”
func PrintAnything[T any](thing T) {
fmt.Println(thing)
}
// Works with everything!
PrintAnything(42) // number
PrintAnything("hello") // string
PrintAnything([]int{1}) // slice
Fun fact: any is just a nickname for interface{}. Same thing, prettier name!
The comparable Constraint
comparable means “I can check if two things are equal.”
Why does this matter? Some things in Go can’t be compared:
- Slices ❌
- Maps ❌
- Functions ❌
But these can be compared:
- Numbers ✅
- Strings ✅
- Booleans ✅
- Structs (if all fields are comparable) ✅
Example: Finding an Item
// Only accepts comparable types
func Contains[T comparable](
list []T,
target T,
) bool {
for _, item := range list {
if item == target { // == needs comparable!
return true
}
}
return false
}
// This works!
nums := []int{1, 2, 3}
found := Contains(nums, 2) // true
// This would NOT compile:
// slices := [][]int{{1}, {2}}
// Contains(slices, []int{1}) // ERROR!
Quick Comparison
| Constraint | Accepts | Use When |
|---|---|---|
any |
Everything | You don’t need to compare or do math |
comparable |
Types you can use == on |
You need to check equality |
4. Generic Functions: Your Swiss Army Knife 🔪
What Are Generic Functions?
A generic function is a function with type parameters. It’s like a recipe that works with different ingredients!
Regular function: “Makes chocolate cake” 🎂 Generic function: “Makes ANY flavor cake you want!” 🎂🍰🧁
Anatomy of a Generic Function
func FunctionName[TypeParams Constraint](
regularParams,
) returnType {
// body
}
Real Examples
Example 1: Swap Any Two Things
func Swap[T any](a, b T) (T, T) {
return b, a
}
// Usage
x, y := Swap(1, 2) // y=1, x=2
s1, s2 := Swap("a", "b") // s2="a", s1="b"
Example 2: Get Keys From Any Map
func Keys[K comparable, V any](
m map[K]V,
) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// Usage
ages := map[string]int{"Alice": 30}
names := Keys(ages) // ["Alice"]
Example 3: Find Maximum Value
type Ordered interface {
int | float64 | string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Usage
Max(5, 10) // 10
Max("a", "z") // "z"
Max(3.14, 2.71) // 3.14
The Magic Flow
graph TD A["Write Generic Function"] --> B["Add Type Parameter"] B --> C["Set Constraint"] C --> D["Use in Function Body"] D --> E["Call with Any Valid Type"] E --> F["Go Figures Out the Type!"]
Type Inference: Go’s Smart Brain 🧠
When you call a generic function, Go is smart enough to figure out the type automatically!
// You CAN write this:
result := Max[int](5, 10)
// But you don't have to! Go figures it out:
result := Max(5, 10) // Go knows it's int
This is called type inference. Go looks at what you pass in and says, “Ah, those are ints, so T must be int!”
Summary: Your Generics Toolkit
| Concept | What It Does | Syntax |
|---|---|---|
| Type Parameter | A placeholder for any type | [T] |
| Type Constraint | Rules for what types are allowed | [T SomeRule] |
| any | Accept any type | [T any] |
| comparable | Accept types that support == |
[T comparable] |
| Generic Function | One function that works with many types | func Name[T C](...) |
The Confidence Boost 🚀
You now understand the four pillars of Go generics:
- Type Parameters → Name your flexible types with
[T] - Type Constraints → Set rules with interfaces or
| - any & comparable → Use Go’s built-in helpers
- Generic Functions → Write once, use everywhere
Before generics, Go developers wrote the same code over and over for different types. Now, you write it once and it works with everything that fits your rules.
That’s not just coding—that’s magic! ✨
Go forth and make your code generic! One function to rule them all. 🏆
