Part 1 — One-Page Summary
What is Generic Programming?
Generic programming means writing code that can work with different data types without rewriting the same logic for each type.
Without generics, you might write:
func PrintInt(value int) {
fmt.Println(value)
}
func PrintString(value string) {
fmt.Println(value)
}
The logic is identical, but the types are different.
With generics, you can write one function:
func Print[T any](value T) {
fmt.Println(value)
}
Now it works with:
Print(10)
Print("Hello")
Print(true)
Print(3.14)
What is a Generic Function?
A generic function is a function that uses one or more type parameters.
Example syntax:
func FunctionName[T Constraint](value T) {
}
Here:
T = Type parameter
Constraint = Which types T is allowed to represent
For example:
func Print[T any](value T)
means:
T.
One Easy Complete Example
package main
import "fmt"
// Print is a GENERIC function.
//
// [T any]
//
// T = type parameter
//
// any = constraint
//
// "any" means T can represent any type.
//
// value T means:
// value will have whatever type T becomes.
func Print[T any](value T) {
// fmt.Println can print the value
// regardless of whether T is:
//
// int
// string
// bool
// float64
// struct
// etc.
fmt.Println(value)
}
func main() {
// Here Go understands:
//
// T = int
Print(100)
// Here:
//
// T = string
Print("Hello Go")
// Here:
//
// T = bool
Print(true)
// Here:
//
// T = float64
Print(10.5)
// Output:
//
// 100
// Hello Go
// true
// 10.5
}
Easy Mental Model
Normal function:
func Double(value int)
Works only with:
int
Generic function:
func Something[T ...](value T)
T becomes the required type
for that particular call.
Think of T as a type placeholder:
T = int
T = string
T = float64
T = User
depending on how the generic code is used.
Part 2 — Generic Programming Using 5W1H
1. WHAT — What Are Generics?
Generics let functions and types operate on multiple related types while keeping compile-time type checking.
Without generics:
package main
// Same logic for int.
func FirstInt(values []int) int {
return values[0]
}
// Same logic repeated for string.
func FirstString(values []string) string {
return values[0]
}
func main() {}
With generics:
package main
// T represents the element type.
//
// []T means:
// "a slice containing values of type T"
//
// The return type T means:
// return one value of the same type.
func First[T any](values []T) T {
// values[0] has type T,
// so it can be returned as T.
return values[0]
}
func main() {
numbers := []int{10, 20, 30}
// T becomes int.
firstNumber := First(numbers)
words := []string{"Go", "Java", "Rust"}
// T becomes string.
firstWord := First(words)
_, _ = firstNumber, firstWord
}
Instead of:
FirstInt()
FirstString()
FirstFloat()
FirstUser()
we can have:
First[T]()
2. WHY — Why Do We Need Generics?
The main reason is to avoid repeating the same algorithm for different types.
Consider:
package main
// Same logic.
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
// Same logic again.
func MaxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}
func main() {}
The algorithm is identical:
if a > b
return a
return b
Only the types are different.
Generics allow us to describe the permitted numeric types once.
package main
import "fmt"
// Number is a generic CONSTRAINT.
//
// It says:
//
// T may have an underlying type of:
//
// int
// int64
// float32
// float64
//
// The | symbol means OR.
//
// ~int means:
// int OR a custom defined type
// whose underlying type is int.
type Number interface {
~int | ~int64 | ~float32 | ~float64
}
// T is allowed to be any type
// satisfying Number.
func Max[T Number](a, b T) T {
// This is allowed because every type
// permitted by Number supports >.
if a > b {
return a
}
return b
}
func main() {
// T = int
fmt.Println(Max(10, 20))
// T = float64
fmt.Println(Max(10.5, 7.2))
// Output:
//
// 20
// 10.5
}
Generics are useful when you have:
Same algorithm
+
Different types
3. WHEN — When Should You Use Generics?
Use generics when the same logic genuinely works for multiple types.
Good example:
package main
import "fmt"
// Contains checks whether a slice
// contains a target value.
//
// comparable means T supports:
//
// ==
// !=
//
// We need == below,
// so comparable is the correct constraint.
func Contains[T comparable](
values []T,
target T,
) bool {
// Each item has type T.
for _, value := range values {
// Allowed because T is comparable.
if value == target {
return true
}
}
return false
}
func main() {
numbers := []int{10, 20, 30}
fmt.Println(
Contains(numbers, 20),
)
names := []string{
"Alice",
"Bob",
}
fmt.Println(
Contains(names, "Bob"),
)
// Output:
//
// true
// true
}
Good generic use cases include:
Contains
Min / Max
Sorting helpers
Collection utilities
Reusable data structures
Map / Filter style utilities
Sets
Stacks
Queues
Cache containers
Algorithms working across related types
Do not use generics just because they exist.
For example:
func Add(a, b int) int {
return a + b
}
is perfectly good if your application only needs integers.
4. WHERE — Where Are Generics Used?
Generics can appear in:
Generic functions
Generic structs/types
Generic containers
Generic algorithms
Reusable libraries
Utility packages
Data processing code
Example: Generic Struct
package main
import "fmt"
// Box can store one value.
//
// T determines what kind of value
// this particular Box stores.
type Box[T any] struct {
// Value has type T.
Value T
}
func main() {
// T = int
numberBox := Box[int]{
Value: 100,
}
// T = string
textBox := Box[string]{
Value: "Hello",
}
fmt.Println(numberBox.Value)
fmt.Println(textBox.Value)
// Output:
//
// 100
// Hello
}
So:
Box[int]
and:
Box[string]
use the same generic structure with different element types.
5. WHO — Who Should Use Generics?
Generics are useful for developers building reusable code.
For beginners:
First learn:
functions
structs
methods
interfaces
slices
maps
Then learn:
generics
Use generics when you find yourself thinking:
"I am writing exactly the same code again,
only because the data type changed."
That is often a strong sign that generics may help.
6. HOW — How Do Generic Functions Work?
Now let’s move from basic to advanced.
Level 1 — Basic Generic Function
package main
import "fmt"
// T is our type parameter.
//
// any means T can be any type.
func Show[T any](value T) {
// value has type T.
fmt.Println(value)
}
func main() {
// Go infers T = int.
Show(100)
// Go infers T = string.
Show("Hello")
// Go infers T = bool.
Show(true)
}
General syntax:
func Name[T Constraint](parameter T) {
}
Level 2 — Returning T
package main
import "fmt"
// Identity receives T
// and returns the same type T.
func Identity[T any](value T) T {
return value
}
func main() {
// T = string.
name := Identity("Alice")
// name is string.
fmt.Println(name)
// T = int.
number := Identity(100)
// number is int.
fmt.Println(number)
}
If input is:
string
output is:
string
If input is:
int
output is:
int
Level 3 — Generic Slice Function
package main
import "fmt"
// T represents the element type
// contained inside the slice.
func Last[T any](values []T) T {
// len(values)-1 gives
// the final slice index.
return values[len(values)-1]
}
func main() {
numbers := []int{
10,
20,
30,
}
// T becomes int.
fmt.Println(
Last(numbers),
)
words := []string{
"Go",
"Rust",
"Java",
}
// T becomes string.
fmt.Println(
Last(words),
)
// Output:
//
// 30
// Java
}
Level 4 — Why any Cannot Do Everything
A common beginner mistake is:
package main
// This does NOT work:
//
// func Add[T any](a, b T) T {
// return a + b
// }
//
// Why?
//
// any allows EVERY type.
//
// T could therefore be:
//
// string
// bool
// struct
// []int
//
// Not every possible type supports +.
func main() {}
So if you need an operation like:
+
>
<
==
you must give Go an appropriate constraint.
Level 5 — Creating a Constraint
package main
import "fmt"
// Number describes the types
// our Add function supports.
type Number interface {
// T may be:
//
// int
// int64
// float32
// float64
//
// including custom types
// with these underlying types.
~int |
~int64 |
~float32 |
~float64
}
func Add[T Number](a, b T) T {
// + is safe because every type
// in Number supports addition.
return a + b
}
func main() {
fmt.Println(
Add(10, 20),
)
fmt.Println(
Add(2.5, 3.5),
)
// Output:
//
// 30
// 6
}
Level 6 — Understanding ~
Consider:
type UserID int
UserID is not exactly int.
It is its own named type.
The ~ allows it when the underlying type matches.
package main
import "fmt"
// UserID is its own type,
// but its underlying type is int.
type UserID int
type Integer interface {
// ~int means:
//
// int
//
// OR
//
// any named type whose
// underlying type is int.
~int
}
func Double[T Integer](value T) T {
return value * 2
}
func main() {
var id UserID = 100
// UserID satisfies ~int.
result := Double(id)
fmt.Println(result)
// Output:
//
// 200
}
Easy rule:
int
=
exact int type
~int
=
int and custom types based on int
Level 7 — comparable
Go provides the built-in constraint:
comparable
Use it when you need:
==
!=
Example:
package main
import "fmt"
// T must support == and !=.
func Equal[T comparable](a, b T) bool {
return a == b
}
func main() {
fmt.Println(
Equal(10, 10),
)
fmt.Println(
Equal("Go", "Go"),
)
fmt.Println(
Equal("Go", "Rust"),
)
// Output:
//
// true
// true
// false
}
Level 8 — Two Type Parameters
A generic function can have multiple type parameters.
package main
import "fmt"
// K and V are two separate type parameters.
//
// K might be string.
//
// V might be int.
func PrintPair[K any, V any](
key K,
value V,
) {
fmt.Println(key, value)
}
func main() {
// K = string
// V = int
PrintPair(
"Age",
30,
)
// K = int
// V = string
PrintPair(
1,
"Alice",
)
// Output:
//
// Age 30
// 1 Alice
}
Level 9 — Generic Function Returning Different Type
The output type does not have to be the same as the input type.
package main
import (
"fmt"
"strconv"
)
// T = input element type.
//
// R = output element type.
//
// transform receives T
// and converts it into R.
func Map[T any, R any](
values []T,
transform func(T) R,
) []R {
// Create result slice
// with the same length.
result := make(
[]R,
len(values),
)
for i, value := range values {
// Convert T into R
// using the supplied function.
result[i] = transform(value)
}
return result
}
func main() {
numbers := []int{
10,
20,
30,
}
// Input T = int
//
// Output R = string
result := Map(
numbers,
func(number int) string {
return strconv.Itoa(number)
},
)
fmt.Println(result)
// Output:
//
// [10 20 30]
//
// These are strings inside []string.
}
This is an important real-world generic pattern:
[]T
↓
transform
↓
[]R
Level 10 — Generic Struct with Method
package main
import "fmt"
// Stack can store values of type T.
type Stack[T any] struct {
items []T
}
// The method uses the same T
// belonging to Stack.
func (s *Stack[T]) Push(value T) {
// Add value to the end.
s.items = append(
s.items,
value,
)
}
// Pop returns the last item.
func (s *Stack[T]) Pop() T {
lastIndex := len(s.items) - 1
value := s.items[lastIndex]
s.items = s.items[:lastIndex]
return value
}
func main() {
// Stack storing integers.
numbers := Stack[int]{}
numbers.Push(10)
numbers.Push(20)
fmt.Println(
numbers.Pop(),
)
// Stack storing strings.
words := Stack[string]{}
words.Push("Go")
words.Push("Rust")
fmt.Println(
words.Pop(),
)
// Output:
//
// 20
// Rust
}
This is one of the strongest use cases for generic programming:
Stack[int]
Stack[string]
Stack[User]
Stack[Order]
without writing four different Stack implementations.
Practical Use Cases
Use Case 1 — Contains
func Contains[T comparable](
values []T,
target T,
) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
Useful for:
[]int
[]string
[]UserID
other comparable values
Use Case 2 — Generic Set
package main
// Set uses T as a map key.
//
// Map keys must be comparable,
// therefore T must be comparable.
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(value T) {
s[value] = struct{}{}
}
func (s Set[T]) Contains(value T) bool {
_, exists := s[value]
return exists
}
func main() {
numbers := Set[int]{}
numbers.Add(10)
numbers.Add(20)
_ = numbers.Contains(10)
}
Use Case 3 — Reusable Data Container
package main
// Result can contain different
// types of successful values.
type Result[T any] struct {
Value T
Err error
}
func main() {
// Result containing string.
stringResult := Result[string]{
Value: "Success",
Err: nil,
}
// Result containing int.
numberResult := Result[int]{
Value: 100,
Err: nil,
}
_, _ = stringResult, numberResult
}
Generics vs any
Do not confuse these two.
package main
// Using any:
//
// Input loses its specific static relationship
// inside the API.
func PrintAny(value any) {
_ = value
}
// Using generic T:
//
// T preserves the concrete type relationship.
func Identity[T any](value T) T {
return value
}
func main() {
// Generic version knows:
//
// input = string
// output = string
name := Identity("Alice")
_ = name
}
Think:
any
===
"I accept any value."
T
=
"I work with a specific type,
but that type is chosen when
the generic code is used."
Generics vs Interfaces
package main
// Interface:
//
// Used when the important question is:
//
// "What can this value DO?"
type Saver interface {
Save() error
}
// Generic:
//
// Used when the important question is:
//
// "Can the SAME algorithm work
// with different types?"
func First[T any](values []T) T {
return values[0]
}
func main() {}
Easy distinction:
INTERFACE
---------
Behavior
"What methods do you have?"
GENERICS
--------
Reusable typed algorithm
"What type are we working with?"
When NOT to Use Generics
package main
// This is perfectly clear.
//
// If your application only needs int,
// generics add no real benefit.
func Add(a, b int) int {
return a + b
}
func main() {}
Don’t change simple code into:
func Add[T SomeHugeConstraint](a, b T) T
unless multiple types are actually needed.
Use generics when they:
Reduce real duplication
Improve reusable libraries
Keep type safety
Make algorithms/data structures reusable
Avoid them when they:
Make simple code harder to read
Add unnecessary constraints
Solve a problem you don't actually have
Final 5W1H Cheat Sheet
package main
// WHAT?
//
// Generics allow code to work
// with multiple types.
// ---------------------------------
// WHY?
//
// Avoid repeating the same algorithm
// for int, string, float, User, etc.
// ---------------------------------
// WHEN?
//
// When the same logic genuinely works
// for multiple types.
// ---------------------------------
// WHERE?
//
// Functions
// structs
// containers
// algorithms
// libraries
// utility code
// ---------------------------------
// WHO?
//
// Developers building reusable,
// type-safe code.
// ---------------------------------
// HOW?
//
// Declare a type parameter:
//
// [T Constraint]
func Identity[T any](value T) T {
return value
}
func main() {
_ = Identity(100)
_ = Identity("Go")
}
The 6 Things to Remember
package main
// 1.
//
// T is a type parameter.
//
// Think:
// "placeholder for a type."
// ---------------------------------
// 2.
//
// any means T can be any type.
func Show[T any](value T) {
_ = value
}
// ---------------------------------
// 3.
//
// comparable means T supports:
//
// ==
// !=
func Equal[T comparable](a, b T) bool {
return a == b
}
// ---------------------------------
// 4.
//
// Custom constraints can restrict T.
type Number interface {
~int | ~float64
}
func Add[T Number](a, b T) T {
return a + b
}
// ---------------------------------
// 5.
//
// Multiple type parameters are possible.
func Pair[A any, B any](a A, b B) {
_, _ = a, b
}
// ---------------------------------
// 6.
//
// Use generics when the SAME LOGIC
// should work safely with DIFFERENT TYPES.
func main() {}
One Sentence to Remember
// GENERIC PROGRAMMING:
//
// "Write the algorithm once,
// allow the type to vary,
// while keeping compile-time type safety."
And a generic function is simply:
// A function containing one or more
// type parameters such as T.
func Example[T any](value T) T {
return value
}
The easiest mental model is:
T = a blank space for a TYPE.
Call:
Identity(100)
T becomes:
int
Call:
Identity("Go")
T becomes:
string
Same function.
Different types.
No duplicated logic.