1. What are Generics?
Generics let you write one piece of code that works with multiple types while keeping Go’s compile-time type safety.
Suppose you want a function that checks whether a slice contains a value.
Without generics, you might write:
ContainsInt(...)
ContainsString(...)
ContainsFloat(...)
With generics, you can write:
Contains(...)
once and use it with different types.
The main idea is:
Write the algorithm once. Let the type vary.
2. Why do we need Generics?
Imagine these two functions:
func ContainsInt(items []int, target int) bool {
// ...
}
func ContainsString(items []string, target string) bool {
// ...
}
The logic is identical.
Only the type changes.
Generics remove this duplication.
Instead, we can say:
func Contains[T comparable](items []T, target T) bool
Here T means:
“There will be a type here, but I don’t need to know exactly which type while writing this function.”
T might later become:
int
string
float64
bool
depending on how the function is called.
3. Complete Copy-Paste Example
Save this as:
main.go
Then run:
go run main.go
package main
import "fmt"
// Contains is a generic function.
//
// T is a TYPE PARAMETER.
//
// "comparable" means T must support:
// ==
// !=
//
// Examples of comparable types:
// int
// string
// float64
// bool
//
// items []T means:
// a slice containing values of type T
//
// target T means:
// the value we are searching for must also be type T
//
// bool means:
// the function returns true or false
func Contains[T comparable](items []T, target T) bool {
// item automatically has type T.
for _, item := range items {
// This works because T is "comparable".
if item == target {
return true
}
}
return false
}
func main() {
// -------------------------
// Example 1: int
// -------------------------
numbers := []int{10, 20, 30, 40}
fmt.Println(Contains(numbers, 20))
// Output: true
fmt.Println(Contains(numbers, 99))
// Output: false
// -------------------------
// Example 2: string
// -------------------------
names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(Contains(names, "Bob"))
// Output: true
fmt.Println(Contains(names, "David"))
// Output: false
}
Output
true
false
true
false
4. How does it work?
The most important line is:
func Contains[T comparable](items []T, target T) bool
Break it into pieces.
| Part | Meaning |
|---|---|
func | Create a function |
Contains | Function name |
[T comparable] | Declare generic type T |
T | Placeholder for a real type |
comparable | Restricts T to types supporting == and != |
items []T | Slice containing T values |
target T | Value of the same type T |
bool | Function returns true or false |
The generic-specific part is:
[T comparable]
You can mentally read it as:
“This function works with some type called
T, as long as values of T can be compared.”
5. What is T?
T is simply a type parameter.
It is a placeholder for a real type.
For example:
Contains(numbers, 20)
numbers is:
[]int
So Go understands:
T = int
Therefore, conceptually, the function behaves like:
func Contains(items []int, target int) bool
But then we call:
Contains(names, "Bob")
names is:
[]string
So now Go understands:
T = string
Conceptually:
func Contains(items []string, target string) bool
You wrote one function, but Go can safely use it with different types.
6. Type inference
Notice that we wrote:
Contains(numbers, 20)
We did not write:
Contains[int](numbers, 20)
Both forms are possible:
Contains[int](numbers, 20)
But normally you don’t need to specify int.
Go looks at:
numbers
and sees:
[]int
so it automatically determines:
T = int
This is called type inference.
7. What is comparable?
Look again:
[T comparable]
comparable is a built-in Go constraint.
It means:
T must be a type that can be compared using
==and!=.
For example:
10 == 20
valid.
"Alice" == "Bob"
valid.
true == false
valid.
Therefore these types work well with our function:
int
string
bool
float64
Our function contains:
if item == target
So Go needs to know that T supports ==.
That is why we use:
comparable
instead of:
any
8. What is any?
Another very common generic constraint is:
any
Example syntax:
func Something[T any](value T)
any means:
T can be any type.
The difference is:
| Constraint | Meaning |
|---|---|
T any | T can be practically any Go type |
T comparable | T must support == and != |
Our function needs:
item == target
Therefore:
comparable
is the correct choice.
9. Visual Model
flowchart LR
A["Contains(numbers, 20)"] --> B["Go sees []int"]
B --> C["T = int"]
D["Contains(names, 'Bob')"] --> E["Go sees []string"]
E --> F["T = string"]
C --> G["Same Contains function"]
F --> G
Think of T as an empty type slot.
First call:
T → int
Second call:
T → string
Same algorithm.
Different types.
10. Why not just use any everywhere?
Before generics, code sometimes accepted values using:
interface{}
Modern Go has the equivalent alias:
any
You could write something like:
func Something(value any)
but now the function does not preserve much information about the actual type.
Generics are different.
Consider:
func Contains[T comparable](items []T, target T)
The compiler knows that:
items contains T
target is T
The two values must therefore agree on their type.
That relationship is one of the major benefits of generics.
11. Normal Functions vs any vs Generics
| Feature | Concrete Function | any / interface{} | Generics |
|---|---|---|---|
| Works with multiple types | ❌ | ✅ | ✅ |
| Preserves type relationships | ✅ | ❌ | ✅ |
| Compile-time type checking | ✅ | Limited for operations on underlying values | ✅ |
| Often requires type assertions | ❌ | ✅ | ❌ |
| Avoids duplicate algorithms | ❌ | ✅ | ✅ |
| Good choice for reusable algorithms | Sometimes | Sometimes | ✅ |
For example, if an algorithm should work identically for:
[]int
[]string
[]float64
generics are often a natural solution.
12. When should you use Generics?
Use generics when the same algorithm needs to work with several types.
Good examples include:
Contains
Min
Max
Filter
Map
Stack
Queue
Set
For example:
Contains int
Contains string
Contains float64
is a strong signal that generics may help.
Instead of:
ContainsInt()
ContainsString()
ContainsFloat()
you can have:
Contains()
13. When should you NOT use Generics?
Don’t use generics simply because they exist.
If your function only works with strings:
func NormalizeUsername(name string) string
there is usually no reason to change it to:
func NormalizeUsername[T ...]
Keep the concrete version.
A useful rule is:
Use generics when the algorithm is the same but the data type changes.
14. Where are Generics commonly used?
You will commonly encounter generics in:
Reusable utility functions
Collections
Stacks
Queues
Sets
Data structures
Algorithms
Libraries
Container types
Reusable APIs
For example, a stack could hold:
Stack[int]
Stack[string]
Stack[User]
while sharing one stack implementation.
15. The syntax you really need to remember
Start with this pattern:
func FunctionName[T constraint](value T) {
}
Example:
func Print[T any](value T) {
fmt.Println(value)
}
Or when comparison is required:
func Contains[T comparable](items []T, target T) bool {
}
The important pattern is:
[T constraint]
where:
T
│
└── type parameter
constraint
│
└── tells Go what T is allowed to be
16. Mental Model
Whenever you see:
[T comparable]
don’t make it complicated.
Read it as:
“T represents a type.”
Then:
items []T
means:
“items is a slice of that type.”
And:
target T
means:
“target must be that same type.”
So:
func Contains[T comparable](items []T, target T) bool
means:
Create a Contains function that works with any comparable type, where the slice and target use the same type.
17. Generics in one picture
WITHOUT GENERICS
[]int ──→ ContainsInt()
[]string ──→ ContainsString()
[]float64 ──→ ContainsFloat()
WITH GENERICS
[]int ─┐
[]string ├──→ Contains[T]()
[]float64 ┘
One implementation.
Multiple types.
18. What / Why / When / How / Where Summary
| Question | Answer |
|---|---|
| What? | Generics let functions and types work with multiple data types. |
| Why? | To avoid writing the same algorithm repeatedly for different types. |
| When? | When the logic stays the same but the type changes. |
| How? | Declare a type parameter such as [T any] or [T comparable]. |
| Where? | Utility functions, algorithms, collections and reusable data structures. |
19. Three things to remember
If you remember only three things, remember these:
1. T is a placeholder for a type
[T ...]
Think:
T = some type
2. A constraint controls what T can be
[T any]
means almost any type.
[T comparable]
means types supporting:
==
!=
3. Generics are useful when logic stays the same
Instead of:
ContainsInt
ContainsString
ContainsFloat
write:
Contains[T]
Final Takeaway
The core idea of Go generics is extremely simple:
Same logic
+
Different types
=
Generics
For this example:
func Contains[T comparable](items []T, target T) bool
T changes:
T = int
T = string
T = bool
...
while the Contains algorithm stays exactly the same.
Once this idea feels natural, the more advanced parts of Go generics—generic structs, multiple type parameters, custom constraints, type sets, and ~ constraints—become much easier to understand.