Generics in Go: Your Friendly Guide to Reusable Code

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy. Go’s simplicity and performance make it a developer favorite, but until Go 1.18, it lacked generics. Generics let you write flexible, reusable code while keeping type safety. Want to write one function that handles multiple types or avoid code duplication? Generics are here to help. This guide covers what generics are, how to use them, and why they’re useful. I'll share full programming examples, tables, and tips to make it clear. What Are Generics and Why Do They Matter? Generics let you write functions, types, and methods that work with multiple types without losing Go’s type safety. Before generics, you’d either duplicate code for each type or use interface{} with type assertions, which was messy and unsafe. Key benefits: Code reuse: One function or type works for many types. Type safety: Catch type errors at compile time. Cleaner code: No repetitive code or risky type casting. For example, a function to find the maximum of two values would need separate versions for int, float64, etc., without generics. Now, you can write it once using a custom constraint for ordered types. Example: package main import "fmt" type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ~string } func Max[T Ordered](a, b T) T { if a > b { return a } return b } func main() { fmt.Println(Max(10, 20)) // Output: 20 fmt.Println(Max(3.14, 2.71)) // Output: 3.14 fmt.Println(Max("go", "golang")) // Output: golang } Here, Ordered is a custom constraint that includes types supporting comparison operators (>, , use comparable or a custom constraint. Building Generic Types Generics also work with structs, interfaces, and other types. This is great for data structures like lists, stacks, or trees that need to handle any type. Example: A generic Stack struct (copy/paste and run): package main import "fmt" 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 } func main() { intStack := Stack[int]{} intStack.Push(42) intStack.Push(100) value, ok := intStack.Pop() fmt.Println(value, ok) // Output: 100 true strStack := Stack[string]{} strStack.Push("hello") strStack.Push("world") str, ok := strStack.Pop() fmt.Println(str, ok) // Output: world true } Why this matters: Generic types let you create reusable data structures without duplicating code or using interface{}. The Stack[T] works for any type, keeping your code clean. Learn more: Go Generics Tutorial Using Constraints to Limit Types Constraints control which types a generic function or type can use. Go offers any and comparable, but you can define custom constraints with interfaces. Example: A custom constraint for numeric types: package main import "fmt" type Number interface { ~int | ~float64 | ~float32 } func Sum[T Number](numbers []T) T { var total T for _, n := range numbers { total += n } return total } func main() { ints := []int{1, 2, 3} floats := []float64{1.5, 2.5, 3.5} fmt.Println(Sum(ints)) // Output: 6 fmt.Println(Sum(floats)) // Output: 7.5 } Key points: The ~ allows types with int, float64, etc., as their underlying type (e.g., type MyInt int). Use | to combine multiple types. Table: Constraint Examples Constraint Example Allowed Types `~int ~float64` interface{ String() string } Any type with a String() method comparable Types supporting == and != Tip: Choose the tightest constraint to make your code safe and clear. Type Inference: Write Less, Do More Go’s type inference simplifies generics. YouOften don’t need to specify the type when calling a generic function—Go infers it from the arguments. Example (copy/paste and run): package main import "fmt" func Print[T any](value T) { fmt.Println(value) } func main() { Print(42) // Output: 42 Print("hello") // Output: hello } Sometimes, you need explicit types, like with generic types or interfaces: Example: package main import "fmt" func Process[T comparable](a, b T) bool { return a == b } func main() { var x interface{} = 42 var y interface{} = 42 result := Process[int](x.(int), y.(int)) // Explicit type needed fmt.Println(result) // Output: true } When inference fails: With slices of interfaces. When using generic types directly (e.g., Stack[T]). Tip: Lean on inference for simple cases, but spe

Apr 21, 2025 - 19:32
 0
Generics in Go: Your Friendly Guide to Reusable Code

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.

Go’s simplicity and performance make it a developer favorite, but until Go 1.18, it lacked generics.

Generics let you write flexible, reusable code while keeping type safety. Want to write one function that handles multiple types or avoid code duplication? Generics are here to help.

This guide covers what generics are, how to use them, and why they’re useful.

I'll share full programming examples, tables, and tips to make it clear.

What Are Generics and Why Do They Matter?

Generics let you write functions, types, and methods that work with multiple types without losing Go’s type safety. Before generics, you’d either duplicate code for each type or use interface{} with type assertions, which was messy and unsafe.

Key benefits:

  • Code reuse: One function or type works for many types.
  • Type safety: Catch type errors at compile time.
  • Cleaner code: No repetitive code or risky type casting.

For example, a function to find the maximum of two values would need separate versions for int, float64, etc., without generics. Now, you can write it once using a custom constraint for ordered types.

Example:

package main

import "fmt"

type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64 | ~string
}

func Max[T Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(10, 20))        // Output: 20
    fmt.Println(Max(3.14, 2.71))    // Output: 3.14
    fmt.Println(Max("go", "golang")) // Output: golang
}

Here, Ordered is a custom constraint that includes types supporting comparison operators (>, <, etc.). The ~ allows custom types with these underlying types (e.g., type MyInt int). This function works with int, float64, string, or other ordered types.

Learn more: Go 1.18 Release Notes

Understanding Type Parameters

Type parameters are the heart of generics. They’re placeholders for types, defined in square brackets [T] after a function or type name. Constraints limit which types can be used.

Syntax:

func FunctionName[T Constraint](param T) T {
    // Code here
}
  • T is the type parameter.
  • Constraint specifies allowed types (e.g., comparable, any, or a custom interface).

Example:

package main

import "fmt"

func Print[T any](value T) {
    fmt.Println(value)
}

func main() {
    Print(42)           // Output: 42
    Print("hello")      // Output: hello
    Print(struct{}{})   // Output: {}
}

Here, T any means T can be any type. You can call Print with int, string, or even structs.

Table: Common Constraints

Constraint Description Example Types
any Any type at all int, string, structs, etc.
comparable Types that support == and != int, string, bool
Custom Interface User-defined interface with methods Custom structs implementing it

Tip: Use any for flexibility, but tighter constraints like comparable for specific operations.

Writing Generic Functions

Generic functions let you write one function that works with multiple types. The key is choosing the right constraint to support the operations you need.

Example: A generic Swap function (copy/paste and run):

package main

import "fmt"

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

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

    s1, s2 := Swap("hi", "world")
    fmt.Println(s1, s2) // Output: world hi
}

Real-world use case: A generic Filter function to keep elements matching a condition (copy/paste and run):

package main

import "fmt"

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
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // Output: [2 4]

    strs := []string{"cat", "dog", "bird"}
    short := Filter(strs, func(s string) bool { return len(s) == 3 })
    fmt.Println(short) // Output: [cat dog]
}

Note: The any constraint works here because we only store and return T. For operations like >, use comparable or a custom constraint.

Building Generic Types

Generics also work with structs, interfaces, and other types. This is great for data structures like lists, stacks, or trees that need to handle any type.

Example: A generic Stack struct (copy/paste and run):

package main

import "fmt"

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
}

func main() {
    intStack := Stack[int]{}
    intStack.Push(42)
    intStack.Push(100)
    value, ok := intStack.Pop()
    fmt.Println(value, ok) // Output: 100 true

    strStack := Stack[string]{}
    strStack.Push("hello")
    strStack.Push("world")
    str, ok := strStack.Pop()
    fmt.Println(str, ok) // Output: world true
}

Why this matters: Generic types let you create reusable data structures without duplicating code or using interface{}. The Stack[T] works for any type, keeping your code clean.

Learn more: Go Generics Tutorial

Using Constraints to Limit Types

Constraints control which types a generic function or type can use. Go offers any and comparable, but you can define custom constraints with interfaces.

Example: A custom constraint for numeric types:

package main

import "fmt"

type Number interface {
    ~int | ~float64 | ~float32
}

func Sum[T Number](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    ints := []int{1, 2, 3}
    floats := []float64{1.5, 2.5, 3.5}
    fmt.Println(Sum(ints))   // Output: 6
    fmt.Println(Sum(floats)) // Output: 7.5
}

Key points:

  • The ~ allows types with int, float64, etc., as their underlying type (e.g., type MyInt int).
  • Use | to combine multiple types.

Table: Constraint Examples

Constraint Example Allowed Types
`~int ~float64`
interface{ String() string } Any type with a String() method
comparable Types supporting == and !=

Tip: Choose the tightest constraint to make your code safe and clear.

Type Inference: Write Less, Do More

Go’s type inference simplifies generics. YouOften don’t need to specify the type when calling a generic function—Go infers it from the arguments.

Example (copy/paste and run):

package main

import "fmt"

func Print[T any](value T) {
    fmt.Println(value)
}

func main() {
    Print(42)      // Output: 42
    Print("hello") // Output: hello
}

Sometimes, you need explicit types, like with generic types or interfaces:

Example:

package main

import "fmt"

func Process[T comparable](a, b T) bool {
    return a == b
}

func main() {
    var x interface{} = 42
    var y interface{} = 42
    result := Process[int](x.(int), y.(int)) // Explicit type needed
    fmt.Println(result) // Output: true
}

When inference fails:

  • With slices of interfaces.
  • When using generic types directly (e.g., Stack[T]).

Tip: Lean on inference for simple cases, but specify types for complex scenarios.

Avoiding Common Generics Pitfalls

Generics are powerful, but they have traps. Here are common issues and fixes.

1. Overusing generics:

  • Don’t use generics when a simple function or interface works. A function for just string doesn’t need generics.
  • Fix: Only use generics for code reused across types.

2. Wrong constraints:

  • Using any when you need + or > causes compile errors.
  • Fix: Use constraints like Number or comparable.

3. Nil and zero values:

  • Generic types may return zero values (e.g., 0 for int, "" for string). Handle them carefully.
  • Example (copy/paste and run):
package main

import "fmt"

func GetFirst[T any](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
    return slice[0]
}

func main() {
    nums := []int{}
    fmt.Println(GetFirst(nums)) // Output: 0

    strs := []string{}
    fmt.Println(GetFirst(strs)) // Output: ""
}

4. Performance:

  • Generics generate specialized code for each type, increasing binary size.
  • Fix: Test performance for critical code.

Learn more: Why Generics

Real-World Example: A Generic Map Function

Let’s wrap up with a practical Map function that transforms a slice of one type into another.

Example:

package main

import (
    "fmt"
    "strings"
)

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

func main() {
    nums := []int{1, 2, 3}
    doubles := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubles) // Output: [2 4 6]

    strs := []string{"a", "b", "c"}
    uppers := Map(strs, strings.ToUpper)
    fmt.Println(uppers) // Output: [A B C]
}

Why this rocks:

  • Works with any input/output type.
  • Reusable for countless transformations.
  • Type-safe and concise.

Tip: Use Map for data transformations like type conversions or calculations.

Generics in Go make your code cleaner and more reusable. From generic functions to type-safe data structures, they solve real problems without complexity. Try a generic function or struct in your next project—you’ll see the difference.

Questions or cool generic patterns? Share in the comments or hit me up on X. Happy coding!