Skip to main content

Overview

Go 1.18 introduced Generics, allowing functions and types to work with any set of types while maintaining type safety. This eliminates the need for code duplication and unsafe type assertions.
Before generics, you had to either duplicate code for each type or use interface{} and lose type safety.

The Problem Generics Solve

// Need separate functions for each type
func PrintInt(v int) {
    fmt.Println(v)
}

func PrintString(v string) {
    fmt.Println(v)
}

func PrintUser(v User) {
    fmt.Println(v)
}

// Or lose type safety with interface{}
func Print(v interface{}) {
    fmt.Println(v)  // No compile-time type checking
}

Basic Syntax: Type Parameters

The syntax [T any] defines a type parameter:
func FunctionName[T TypeConstraint](param T) T {
    // T can be used like any other type
    return param
}
1

Square Brackets [T any]

Declares a type parameter named T with the constraint any.
2

The any Constraint

any is an alias for interface{}, meaning “any type is allowed.”
3

Use T Like a Type

Inside the function, T acts like a normal type.

Generic Functions

Simple Generic Function

task3/main.go
package main

func Identity[T any](a T) T {
	return a
}

func main() {
   Identity(10)        // Returns 10 (int)
   Identity("Hello")   // Returns "Hello" (string)
   Identity(2.67)      // Returns 2.67 (float64)
}
The compiler infers the type parameter automatically from the arguments.

Generic Swap Function

task4/main.go
package main

import "fmt"

func Swap[T any](a, b *T) {
	*a, *b = *b, *a
}

func main() {
	x := 10
	y := 20
	Swap(&x, &y)
	fmt.Println(x, y)  // 20 10

	s1 := "hello"
	s2 := "world"
	Swap(&s1, &s2)
	fmt.Println(s1, s2)  // world hello
}
Without generics, you’d need SwapInt, SwapString, SwapFloat, etc. Or use reflection, which is slow and loses type safety.

Generic Types

You can also create generic structs:
task2/main.go
package main

import "fmt"

type Box[T any] struct {
	value T
}

func main() {
	intBox := Box[int]{value: 10}
	strBox := Box[string]{value: "golang"}

	fmt.Println(intBox.value)  // 10
	fmt.Println(strBox.value)  // golang
}
Real-World Use Case:
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, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Usage
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop()  // val is int, guaranteed

Type Constraints

any is too permissive sometimes. You might need to restrict which types are allowed.

Built-in Constraints

import "golang.org/x/exp/constraints"

// Only numeric types
func Sum[T constraints.Ordered](a, b T) T {
    return a + b
}

Sum(10, 20)       // ✅ Works
Sum(1.5, 2.3)     // ✅ Works
Sum("a", "b")     // ❌ Compile error: strings don't support +

Common Constraints

func Print[T any](v T) {
    fmt.Println(v)
}
Allows any type.

Custom Constraints

You can define your own constraints using interfaces:
// Only types with a String() method
type Stringer interface {
    String() string
}

func PrintString[T Stringer](v T) {
    fmt.Println(v.String())
}

// Usage
type User struct {
    Name string
}

func (u User) String() string {
    return u.Name
}

u := User{Name: "Alice"}
PrintString(u)  // ✅ User has String() method

Union Constraints

Allow specific types using the | operator:
type Number interface {
    int | int64 | float64
}

func Add[T Number](a, b T) T {
    return a + b
}

Add(10, 20)          // ✅ int
Add(1.5, 2.5)        // ✅ float64
Add("a", "b")        // ❌ Compile error: string not in union

Generic Data Structures

Generic Linked List

type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type LinkedList[T any] struct {
    Head *Node[T]
}

func (l *LinkedList[T]) Add(value T) {
    node := &Node[T]{Value: value, Next: l.Head}
    l.Head = node
}

func (l *LinkedList[T]) Print() {
    current := l.Head
    for current != nil {
        fmt.Println(current.Value)
        current = current.Next
    }
}

// Usage
list := LinkedList[int]{}
list.Add(1)
list.Add(2)
list.Add(3)
list.Print()  // 3, 2, 1

Generic Map Functions

func Map[T any, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4}
doubled := Map(numbers, func(n int) int {
    return n * 2
})  // [2, 4, 6, 8]

strings := Map(numbers, func(n int) string {
    return fmt.Sprintf("#%d", n)
})  // ["#1", "#2", "#3", "#4"]

Generic Filter

func Filter[T any](slice []T, predicate func(T) bool) []T {
    result := []T{}
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5, 6}
evens := Filter(numbers, func(n int) bool {
    return n%2 == 0
})  // [2, 4, 6]

Type Inference

Go can infer type parameters in most cases:
result := Max[int](10, 20)
You only need explicit type parameters when:
  • The compiler can’t infer the type
  • You want to be explicit for clarity
  • You’re creating a value of a generic type
// Explicit needed here
stack := Stack[int]{}  // What would T be without [int]?

// Inferred here
stack.Push(42)  // Go knows T is int from the stack type

When to Use Generics

1. Data Structures
type Stack[T any] struct { /* ... */ }
type Queue[T any] struct { /* ... */ }
type Tree[T comparable] struct { /* ... */ }
2. Algorithms
func Sort[T constraints.Ordered](slice []T) { /* ... */ }
func BinarySearch[T comparable](slice []T, target T) int { /* ... */ }
3. Utility Functions
func Map[T, U any](slice []T, fn func(T) U) []U { /* ... */ }
func Filter[T any](slice []T, fn func(T) bool) []T { /* ... */ }
func Reduce[T, U any](slice []T, init U, fn func(U, T) U) U { /* ... */ }
4. Type-Safe Wrappers
type Result[T any] struct {
    Value T
    Err   error
}

type Option[T any] struct {
    value   T
    present bool
}
Don’t add generics just because you can. Use them when they eliminate real duplication or improve type safety.

Generics vs Interfaces

Use Generics When:
  • You need the exact same logic for multiple types
  • You want compile-time type safety
  • You’re building data structures or algorithms
// Generic: Type is known at compile time
func First[T any](slice []T) T {
    return slice[0]
}
Use Interfaces When:
  • You need runtime polymorphism
  • Different types have different implementations
  • You want dependency injection
// Interface: Implementation varies by type
type Shape interface {
    Area() float64
}

func TotalArea(shapes []Shape) float64 {
    total := 0.0
    for _, s := range shapes {
        total += s.Area()  // Different for each shape
    }
    return total
}

Best Practices

1

Use descriptive type parameter names

// ❌ Unclear
func Process[A any, B any](a A, b B) {}

// ✅ Clear
func Convert[Input any, Output any](in Input) Output {}

// ✅ Standard conventions
func Map[T any, U any](slice []T, fn func(T) U) []U {}
2

Keep constraints minimal

// ❌ Too restrictive
func Print[T fmt.Stringer](v T) {
    fmt.Println(v.String())
}

// ✅ More flexible
func Print[T any](v T) {
    fmt.Println(v)
}
3

Prefer type inference

// ❌ Explicit (unnecessary)
result := Max[int](10, 20)

// ✅ Inferred (cleaner)
result := Max(10, 20)
4

Don't over-generalize

Only add generics when you have at least 2-3 use cases.
“A little copying is better than a little dependency” - Rob Pike

Real-World Example: Result Type

type Result[T any] struct {
    Value T
    Err   error
}

func (r Result[T]) IsOk() bool {
    return r.Err == nil
}

func (r Result[T]) Unwrap() (T, error) {
    return r.Value, r.Err
}

// Usage
func FetchUser(id int) Result[User] {
    user, err := db.GetUser(id)
    return Result[User]{Value: user, Err: err}
}

result := FetchUser(123)
if result.IsOk() {
    fmt.Println(result.Value.Name)
} else {
    fmt.Println("Error:", result.Err)
}

Key Takeaways

  • Generics provide type-safe code reuse without duplication
  • Use [T any] for type parameters
  • Constraints limit which types are allowed (any, comparable, custom interfaces)
  • Type inference works in most cases—explicit types rarely needed
  • Use generics for data structures and algorithms, not everything
  • Interfaces and generics solve different problems—use the right tool

Next Steps