Skip to main content

Command Palette

Search for a command to run...

Understanding Generics in Go: The Power of Type Parameters

Exploring How Type Parameters Enhance Go's Generics Feature

Updated
5 min read
Understanding Generics in Go: The Power of Type Parameters

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

  1. any

    Represents all possible types.
    It’s equivalent to an empty interface (interface{}) in older Go versions.

    Example:

  1.  func PrintValue[T any] (v T) {
         fmt.Println(v)
     }
    

    Here, T can be any type — string, int, struct etc

  2. 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")) // false
    

    This prevents you from accidentally using non-comparable types like slices or maps.

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