Go Tutorials: Generics

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.

PartMeaning
funcCreate a function
ContainsFunction name
[T comparable]Declare generic type T
TPlaceholder for a real type
comparableRestricts T to types supporting == and !=
items []TSlice containing T values
target TValue of the same type T
boolFunction 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:

ConstraintMeaning
T anyT can be practically any Go type
T comparableT 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

FeatureConcrete Functionany / interface{}Generics
Works with multiple types
Preserves type relationships
Compile-time type checkingLimited for operations on underlying values
Often requires type assertions
Avoids duplicate algorithms
Good choice for reusable algorithmsSometimesSometimes

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

QuestionAnswer
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.

Related Posts

Go Tutorials: Go Methods

Go methods become much easier once you understand one idea: A method is simply a function attached to a type. And the important rule is: A receiver…

Read More

Go Tutorials: Modules, Packages, Subpackages, Submodules, and Workspaces

This tutorial gives you one complete mental model for: The most important idea is: And sometimes: These two designs are very different. 1. What is a Go…

Read More

Go Tutorials: Numeric Types Beginner Tutorial

Go has four main families of numeric types: The simplest way to remember them is: Type family Stores Example int Whole numbers, positive or negative -10, 0,…

Read More

Go Tutorials: Go Testing, Benchmarking, and Profiling

These three topics become much easier once you separate the questions they answer: Topic Main question Main Go tool Result Testing Does my code work correctly? go…

Read More

Go Concurrency Management Made Simple

This tutorial covers: The goal is: One concept → one purpose → one complete runnable example. Every example is independent. Save any example as: and run: 0….

Read More

Go Tutorials: Concurrency Made Simple

This tutorial covers: The goal is simple: One concept → one mental model → one complete example. Every example is independent. You can copy it into main.go…

Read More