Go Generics in 2026: Type Parameters, Constraints and Interview Questions

Master Go generics for technical interviews with questions on type parameters, constraints, the tilde operator, and practical implementations like generic caches.

Go generics type parameters and constraints diagram

Go 1.18 introduced generics to the language in March 2022, and since then, the feature has matured through Go 1.21, 1.22, and beyond. This article covers essential Go generics interview questions for 2026, from basic type parameters to advanced constraint patterns that hiring managers commonly ask about.

What Are Go Generics?

Go generics enable writing functions and types that work with any data type while maintaining compile-time type safety. Unlike interfaces that use runtime type assertions, generics resolve types at compile time, providing better performance and earlier error detection.

Understanding Type Parameters in Go

Type parameters form the foundation of Go generics. A type parameter is a placeholder for a type that gets specified when the generic function or type is used.

generic.gogo
// Basic generic function with type parameter T
func PrintSlice[T any](items []T) {
    for _, item := range items {
        fmt.Println(item)
    }
}

// Usage - type inference determines T automatically
func main() {
    PrintSlice([]int{1, 2, 3})        // T is int
    PrintSlice([]string{"a", "b"})     // T is string
}

The square bracket syntax [T any] declares a type parameter T constrained by any. Go's compiler infers the concrete type from the arguments, eliminating the need for explicit type specification in most cases.

Common Interview Question: What Is the Difference Between any and comparable?

Interviewers frequently ask about the built-in constraints any and comparable. Understanding their differences demonstrates solid generics knowledge.

constraints.gogo
// any: accepts all types (alias for interface{})
func Process[T any](value T) T {
    return value
}

// comparable: only types that support == and !=
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {  // This comparison requires comparable
            return true
        }
    }
    return false
}

// This compiles
Contains([]int{1, 2, 3}, 2)

// This fails: slices are not comparable
// Contains([][]int{{1}, {2}}, []int{1})

The comparable constraint restricts type parameters to types that support equality operators. Slices, maps, and functions are excluded because Go does not define equality for them at the language level.

Creating Custom Type Constraints with Interfaces

Go uses interfaces to define custom constraints, expanding what generic functions can accept. The Go specification defines how type elements work within constraint interfaces.

number.gogo
// Custom constraint using type union
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

// Generic function constrained to numeric types
func Sum[T Number](values []T) T {
    var total T
    for _, v := range values {
        total += v  // + operator works because all Number types support it
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))         // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))    // 4.0
}

Type unions with | specify which exact types satisfy the constraint. This approach provides more control than any while avoiding runtime type assertions.

Interview Question: Explain the ~ Tilde Operator in Constraints

The tilde operator ~ in constraints matches both a type and all types with that underlying type. This question tests understanding of Go's type system.

underlying.gogo
// ~int matches int and any type with int as underlying type
type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Custom type with int as underlying type
type UserID int
type Temperature int64

func Abs[T Signed](value T) T {
    if value < 0 {
        return -value
    }
    return value
}

func main() {
    var id UserID = -42
    var temp Temperature = -10
    
    fmt.Println(Abs(id))    // 42 - works because ~int matches UserID
    fmt.Println(Abs(temp))  // 10 - works because ~int64 matches Temperature
}

Without ~, the constraint int would only match the exact type int, not custom types like UserID. The tilde expands the constraint to include derived types, making generic code more flexible.

Ready to ace your Go interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Generic Types: Structs and Methods

Generic types extend beyond functions to structs and methods. This pattern appears frequently in data structure implementations.

stack.gogo
// Generic Stack type
type Stack[T any] struct {
    items []T
}

// Push adds an element to the stack
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

// Pop removes and returns the top element
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T  // Zero value for type T
        return zero, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func main() {
    intStack := Stack[int]{}
    intStack.Push(10)
    intStack.Push(20)
    
    val, ok := intStack.Pop()  // val=20, ok=true
}

Note that methods on generic types must repeat the type parameter in brackets but cannot introduce new type parameters. The receiver (s *Stack[T]) binds the method to the specific instantiation of Stack.

Interview Question: Why Can't Methods Have Their Own Type Parameters?

Go prohibits additional type parameters on methods, a design decision that surprises developers coming from languages like Java or C#. The Go generics proposal explains this limitation exists to keep the type system tractable.

go
// This is NOT valid Go code
type Container[T any] struct {
    value T
}

// ERROR: methods cannot have type parameters
// func (c *Container[T]) Transform[U any](fn func(T) U) U {
//     return fn(c.value)
// }

// Valid alternative: use a standalone function
func Transform[T, U any](c *Container[T], fn func(T) U) U {
    return fn(c.value)
}

The workaround uses top-level functions with multiple type parameters instead of methods. This design keeps method dispatch simple and avoids complex interactions between receiver types and method type parameters.

Type Inference and Constraint Inference

Go's type inference reduces verbosity when calling generic functions. Understanding when explicit type arguments are required helps write cleaner code.

inference.gogo
func Map[T, R any](input []T, transform func(T) R) []R {
    result := make([]R, len(input))
    for i, v := range input {
        result[i] = transform(v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3}
    
    // Type inference: T=int, R=string inferred from arguments
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("%d", n)
    })
    
    // Explicit types sometimes required for complex cases
    // Map[int, string](numbers, converter)
}

Type inference works from function arguments to type parameters. When the compiler cannot infer types from arguments alone, explicit type arguments in square brackets resolve the ambiguity.

Constraints Package: cmp and slices in the Standard Library

Go 1.21 added the cmp package with the Ordered constraint and comparison functions. The slices package demonstrates idiomatic generic code in the standard library.

stdlib.gogo
import (
    "cmp"
    "slices"
)

func main() {
    numbers := []int{3, 1, 4, 1, 5, 9}
    
    // slices.Sort uses cmp.Ordered constraint internally
    slices.Sort(numbers)  // [1, 1, 3, 4, 5, 9]
    
    // Binary search on sorted slice
    index, found := slices.BinarySearch(numbers, 4)
    
    // cmp.Compare returns -1, 0, or 1
    result := cmp.Compare(3, 5)  // -1
    
    // cmp.Or returns first non-zero value
    value := cmp.Or(0, 0, 42, 100)  // 42
}

The standard library documentation shows how these packages leverage generics for type-safe operations on ordered types. Familiarity with these packages demonstrates practical generics knowledge beyond theoretical understanding.

Interview Question: Implementing a Generic Cache

A common interview exercise asks candidates to implement a generic, thread-safe cache. This tests generics, concurrency with the sync package, and API design.

cache.gogo
import (
    "sync"
    "time"
)

type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]cacheItem[V]
}

type cacheItem[V any] struct {
    value      V
    expiration time.Time
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{
        items: make(map[K]cacheItem[V]),
    }
}

func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = cacheItem[V]{
        value:      value,
        expiration: time.Now().Add(ttl),
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, exists := c.items[key]
    if !exists || time.Now().After(item.expiration) {
        var zero V
        return zero, false
    }
    return item.value, true
}

This implementation uses comparable for keys (map requirement) and any for values. The separate cacheItem struct shows how generic types can nest within each other. Thread safety comes from sync.RWMutex, a pattern covered in Go concurrency interviews.

Zero Values and Type Constraints

Handling zero values in generic code requires understanding how Go initializes variables of parameterized types.

zero.gogo
// Return zero value when slice is empty
func First[T any](slice []T) T {
    if len(slice) == 0 {
        var zero T  // Zero value: 0 for int, "" for string, nil for pointers
        return zero
    }
    return slice[0]
}

// Alternative: return pointer to avoid ambiguity
func FirstOrNil[T any](slice []T) *T {
    if len(slice) == 0 {
        return nil
    }
    return &slice[0]
}

The var zero T pattern creates a zero value for any type T. For types where zero is a valid value (like 0 for integers), the pointer-returning variant distinguishes between "not found" and "found zero."

Advanced: Combining Multiple Constraints

Complex generic functions may require types to satisfy multiple constraints. Go handles this through interface embedding.

combined.gogo
// Constraint requiring both ordering and string conversion
type Stringable interface {
    String() string
}

type OrderedStringable interface {
    cmp.Ordered
    Stringable
}

// Alternative: use type parameters with multiple constraints inline
func PrintSorted[T interface{ cmp.Ordered; fmt.Stringer }](items []T) {
    slices.Sort(items)
    for _, item := range items {
        fmt.Println(item.String())
    }
}

Interface embedding combines constraints, requiring types to implement all embedded interfaces. The inline syntax interface{ A; B } provides the same functionality without declaring a named constraint type.

Conclusion

  • Type parameters with [T any] enable writing reusable, type-safe code without runtime reflection
  • comparable restricts to types supporting == and !=, required for map keys
  • The ~ tilde matches types with a specific underlying type, expanding constraint flexibility
  • Custom constraints use type unions (int | string) to specify exact allowed types
  • Methods cannot have their own type parameters; use top-level functions as an alternative
  • Standard library packages cmp and slices demonstrate idiomatic generic patterns
  • Zero values via var zero T handle empty or missing data in generic functions
  • Thread-safe generic data structures combine generics with sync primitives

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Share

Related articles