Go Tutorials – Generic Programming and Generic Functions in Go

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:

Print can receive a value of any type, and inside this function that type is represented by 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.

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 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…

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