Go Generic Functions

Golang Tutorials


Go generic functions let one function work safely with several related types. You write the algorithm once, describe which types it accepts, and keep compile-time type checking for every call.

Generics have been available since Go 1.18. They are useful for reusable operations on slices, maps, numeric values, and data structures when ordinary functions would otherwise repeat the same logic.

Generic Function Syntax

Place type parameters in square brackets between the function name and its regular parameters. Each type parameter has a constraint that defines the permitted type arguments.

Syntax:

// FunctionName accepts a type argument T that satisfies Constraint.
func FunctionName[T Constraint](value T) T {
    return value
}

The compiler replaces T with a concrete type for each valid call. It rejects an argument that does not satisfy the constraint.

Use the any Constraint

The predeclared any constraint accepts every type. Use it when the function does not need operators or methods that depend on a narrower type set.

Example:

package main

import "fmt"

// First returns the first value and reports whether the slice has an item.
func First[T any](values []T) (T, bool) {
    if len(values) == 0 {
        var zero T
        return zero, false
    }
    return values[0], true
}

func main() {
    name, ok := First([]string{"Amelia", "Ravi"})
    fmt.Println(name, ok)
}

Output:

Amelia true

The zero value works for any possible T. For a string it is an empty string, for a number it is zero, and for a pointer it is nil.

Compare Values with comparable

The comparable constraint accepts types that support == and !=. It is useful for keys, membership checks, and equality-based searches.

Example:

// Index returns the position of target or -1 when it is absent.
func Index[T comparable](values []T, target T) int {
    for i, value := range values {
        if value == target {
            return i
        }
    }
    return -1
}

You can call Index with strings, integers, pointers, and comparable structs. You cannot use a slice or map as T because those types do not support ordinary equality comparison.

Create a Numeric Constraint

Use a type set when the algorithm requires operators such as +. The vertical bar separates permitted type terms. A leading tilde includes named types whose underlying type matches the listed type.

Example:

package main

import "fmt"

// Number accepts common integer and floating-point types.
type Number interface {
    ~int | ~int64 | ~float64
}

// Sum adds a slice while preserving its concrete numeric type.
func Sum[T Number](values []T) T {
    var total T
    for _, value := range values {
        total += value
    }
    return total
}

func main() {
    prices := []int{499, 799, 1200}
    ratings := []float64{4.5, 4.8, 4.7}

    // Type inference chooses int and float64 automatically.
    fmt.Println("Price total:", Sum(prices))
    fmt.Printf("Rating total: %.1f", Sum(ratings))
}

Output:

Price total: 2498
Rating total: 14.0

Click Run Code to execute the complete example. The first call produces an int, while the second produces a float64.

Understand the Tilde in a Constraint

Without a tilde, a type term accepts only that exact declared type. With ~int, the constraint also accepts user-defined types whose underlying type is int.

Example:

type Rupees int

// Number includes Rupees because its underlying type is int.
amounts := []Rupees{250, 400, 125}
total := Sum(amounts)

This keeps domain-specific types useful without forcing you to convert every value back to a built-in type.

Let Go Infer Type Arguments

You can provide a type argument explicitly, but Go often infers it from regular function arguments.

Call Meaning
Sum([]int{2, 4, 6}) The compiler infers int
Sum[float64]([]float64{1.5, 2.5}) The caller supplies float64 explicitly
Index([]string{"a", "b"}, "b") The compiler infers string

Prefer inference when the concrete type is obvious. Explicit type arguments remain helpful when inference cannot determine a type or when you want to make the intended type clear.

Use More Than One Type Parameter

A function can declare several type parameters. Give each one the narrowest constraint required by the implementation.

Example:

// Keys returns all keys from a map in unspecified order.
func Keys[K comparable, V any](items map[K]V) []K {
    keys := make([]K, 0, len(items))
    for key := range items {
        keys = append(keys, key)
    }
    return keys
}

K must be comparable because Go map keys require comparable types. V can be any type because the function does not compare or operate on map values.

Generics and Interfaces

Use an ordinary interface when your function needs behavior expressed through methods. Use a type parameter when the function needs to preserve a concrete type or apply the same operation across a defined set of types.

Need Good choice
Call a shared method on different values Ordinary interface
Return the same concrete type passed by the caller Generic function
Use operators across a limited group of types Generic function with a type-set constraint
Handle unrelated values without type-specific operations any or an ordinary interface, depending on the design

Common Mistakes

  • Using any for arithmetic: any does not permit the + operator. Define a numeric constraint.
  • Making constraints too broad: allow only the operations and types the function truly supports.
  • Replacing simple code: a generic abstraction can be harder to read when only one concrete type is needed.
  • Assuming every type is comparable: slices, maps, and functions cannot be compared with ordinary equality.
  • Ignoring zero values: declare var zero T when a generic function needs the zero value of its result type.

When to Write a Generic Function

Choose a generic function when the same short algorithm appears for several types and callers benefit from preserving their concrete type. Slice utilities, map helpers, numeric aggregation, and reusable containers are common examples.

Start with the simplest concrete function. Generalize it after the repeated type pattern becomes clear. This keeps constraints focused and prevents an abstraction from becoming more complex than the code it replaces.

Conclusion

Go generic functions combine reusable algorithms with compile-time type safety. Declare type parameters in square brackets, choose any or comparable when they fit, and define a type set for operator-based code. Use type inference for clear calls and keep every constraint as narrow as the function requires.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram