Youssef Ameachaq's Blog

Youssef Ameachaq

Generics in Go: A Simple Guide with Examples


Generics in Go allow you to write flexible, reusable code that works with different types while ensuring type safety. They were introduced in Go 1.18 and are especially helpful for functions, structs, or methods that need to operate on multiple data types without duplicating code.


1. What are Generics?

Generics let you write a function or type that can work with any type, rather than a specific one. For example, you can create a function to sum numbers or find the maximum of a list, regardless of whether the numbers are integers, floats, or any other type.

2. Basic Syntax

Here’s how generics look in Go:
func FunctionName[T any](param T) T {
    // your logic here
    return param
}
  • T is a type parameter. You can name it anything, but T is common.
  • any is a built-in type constraint that means “any type.”

3. Generic Functions

Example: A function to get the first item of any slice

package main

import "fmt"

// Generic function
func First[T any](items []T) T {
    return items[0]
}

func main() {
    nums := []int{1, 2, 3}
    words := []string{"hello", "world"}

    fmt.Println(First(nums))  // Output: 1
    fmt.Println(First(words)) // Output: hello
}

Explanation:

  • First[T any]: The function accepts a type T.
  • items []T: The parameter is a slice of any type T.
  • The function returns the first element of the slice, regardless of its type.

4. Generic Structs

You can also define structs with generics. For example, a `Pair` struct to hold two values of the same type:
package main

import "fmt"

// Generic struct
type Pair[T any] struct {
    First  T
    Second T
}

func main() {
    intPair := Pair[int]{First: 1, Second: 2}
    stringPair := Pair[string]{First: "hello", Second: "world"}

    fmt.Println(intPair)      // Output: {1 2}
    fmt.Println(stringPair)   // Output: {hello world}
}

Explanation:

  • Pair[T any]: The struct accepts a type T.
  • You can create pairs of integers, strings, or any other type.

5. Constraints

Constraints specify what kind of types you allow for generics. For example, if you’re working with numbers, you can use the `constraints` package.

Example: A function to sum a slice of numbers

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// Generic function with constraints
func Sum[T constraints.Ordered](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    ints := []int{1, 2, 3, 4}
    floats := []float64{1.1, 2.2, 3.3}

    fmt.Println(Sum(ints))   // Output: 10
    fmt.Println(Sum(floats)) // Output: 6.6
}

Explanation:

  • constraints.Ordered ensures the types are comparable (e.g., integers, floats, or strings).
  • The Sum function works for both integers and floats.

Using a Number Type Constraint

To accept only numeric types (like integers and floats), you can define a custom constraint called Number.

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

// Define a custom Number type constraint
type Number interface {
	constraints.Integer | constraints.Float
}

// Generic function using the Number constraint
func Add[T Number](a, b T) T {
	return a + b
}

func main() {
	fmt.Println(Add(5, 10))         // Works with integers, Output: 15
	fmt.Println(Add(3.2, 4.8))     // Works with floats, Output: 8
	// fmt.Println(Add(5, "hello")) // Compilation error: string not allowed
}

Without constraints (Manual Custom Type Constraint)

package main

import "fmt"

// Define the Number constraint manually
type Number interface {
	int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

// Generic function using the Number constraint
func Multiply[T Number](a, b T) T {
	return a * b
}

func main() {
	fmt.Println(Multiply(4, 5))       // Output: 20 (int)
	fmt.Println(Multiply(2.5, 3.0))  // Output: 7.5 (float64)
	// fmt.Println(Multiply("a", "b")) // Compilation error: string not allowed
}

Explanation:

  • Number is an interface that lists all numeric types you want to allow.
  • The function ensures only types satisfying the Number constraint can be used.
  • Compile-time errors prevent misuse.

6. Multiple Type Parameters

You can have more than one type parameter. For example, a function that swaps two values of any type:
package main

import "fmt"

// Generic function with two type parameters
func Swap[T, U any](a T, b U) (U, T) {
    return b, a
}

func main() {
    x, y := 1, "hello"
    a, b := Swap(x, y)
    fmt.Println(a, b) // Output: hello 1
}

7. Best Practices

- Use generics when you need flexibility and reuse across multiple types. - Avoid overusing them; simple, concrete types are often sufficient. - Leverage constraints to ensure type safety.

Generics in Go make your code cleaner and reduce repetition while maintaining Go’s simplicity and performance. Experiment with these examples to get comfortable!