Understanding Generics in Go: The Power of Type Parameters
Exploring How Type Parameters Enhance Go's Generics Feature

When Go was first introduced, one of the most debated missing features was Generics, the ability to write code that works for any data type without sacrificing type safety.
With Go 1.18, that changed forever.
Generics make Go code more reusable, less repetitive, and safer — all without losing the language’s hallmark simplicity.
Why are Generics needed?
Imagine you want to write a function that finds the minimum value in a slice:
func MinInt(a, b int) int {
if a < b {
return a
}
return b
}
If you later need the same function for float64, you’d have to write another one.
func MinFloat(a,b float64) float64 {
if a < b {
return a
}
return b
}
This is code duplication with the same logic, just different types.
Generics solve this problem.
What Are Generics?
Generics allow you to write functions, structs, and methods that can work with any type.
They do this using type parameters, which are placeholders for types that you define when using the function or struct.
Think of type parameters like "boxes" that hold different types, but still guarantee what kind of things they can hold.
Now let's make the above code example generic so we don't need to create two functions to find the minimum for int and float data types.
func Min[T any](a,b T) T {
if a < b {
return a
}
return b
}
Let’s break it down and understand each keyword:
T → It is a type parameter, just like a variable name but for types.
[T any] → Declares that this function uses a type parameter T, which can be any type.
a, b T → Function arguments that must both be of type T.
The function returns a value of type T.
We have made the function generic for all data types, but there’s a problem.
Not all types can use the < operator (like structs or slices), so it needs some constraints.
Type Constraints
A constraint defines what operations are allowed on a type parameter.
Built-in constraints
any → allows any type
comparable → allows types that can use == and !=
Custom constraints can be created using interfaces
Built-in Constraints in Go
any
Represents all possible types.
It’s equivalent to an empty interface (interface{}) in older Go versions.Example:
func PrintValue[T any] (v T) { fmt.Println(v) }Here, T can be any type — string, int, struct etc
Comparable
Represents all types that can be compared using == and !=.
That includes primitive types like int, string, bool, and even structs if all their fields are comparable.
Example
func AreEauqal[T comparable](a,b T) bool { return a == b }Usage:
fmt.Println(AreEqual(10,10)) // true fmt.Println(AreEqual("go","rust")) // falseThis prevents you from accidentally using non-comparable types like slices or maps.
Custom Constraints
You can create your own constraints using interfaces and type unions.
type Ordered interface {
~int | ~float64 | ~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
In the above code, only ordered types (numbers and strings) can use this Min function.
This is type safety + flexibility combined.
Here:
~means “underlying type of”.The Ordered constraint allows types that are comparable using <.
Let’s dive deep into the ~ operator and underlying types.
The tilde (~) symbol in Go generics is used to match types that have a specific underlying type, even if they are custom type definitions.
What is an Underlying Type?
In Go, when you create a custom type, it's based on another existing type—that's its underlying type.
type MyInt int
type Score float64
Here,
MyInt has an underlying type of int
Score has an underlying type of float64
So even though MyInt and Score are distinct types, they are built on top of primitive types.
Why ~ Matters in Constraints
If you don’t use ~, your constraint only accepts exact matches.
If you use ~, it also accepts types derived from that underlying type.
Without
type Ordered interface {
int | float64
}
Works for int, float64, but fails for MyInt (even though it’s based on int).
With
type Ordered interface {
~int | ~float64
}
Works for int
Works for MyInt
Works for float64
Works for any type whose underlying type is int or float64
Generic Structs
You can also define structs with type parameters.
type Pair[T, U any] struct {
First T
Second U
}
Usage:
p := Pair[string, int]{First: "Age", Second: 25}
fmt.Println(p.First, p.Second)
Generic Methods
You can also attach methods to generic types.
type Stack[T any] struct {
items []T
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() T {
n := len(s.items)
item := s.items[n-1]
s.items = s.items[:n-1]
return item
}
Usage:
s := Stack[int]{}
s.push(10)
s.push(20)
fmt.Println(s.Pop()) // 20
Type Inference
You don’t always have to specify the type manually. Go’s compiler can infer the type from your arguments.
PrintSlice([]string{"Go","Rust","Python","JS"})
If the function is defined as:
func PrintSlice[T any](s[]T) {
for _,v := range s {
fmt.Println(v)
}
}
Go automatically understands that T is a string.
Real-World Use Cases
Utility functions (like Min, Max, contains)
Data structures (Stack, Queue, Tree)
Generic database models or repositories
Writing reusable libraries
Performance and Limitations
Advantages:
Compile-time type safety
No need for reflection or type assertions
Less code duplication
Cleaner, reusable APIs
Limitations:
Slightly more complex syntax
May increase compile time for very large codebases
Not ideal for every use case — sometimes interfaces are simpler
Analogy: Generics as Blueprints
Think of generics as a blueprint for a tool. You design one hammer template, and depending on the type (wood, metal, rubber), Go builds a hammer specialized for that material at compile time.
There's no need to make a separate hammer for each. Just one perfect template works.
Conclusion
Generics bring clarity, reusability, and type safety to Go. They eliminate repetitive code and make libraries more elegant.
If you’ve ever wished you could “write once, use for all types,” then Generics are the answer.




