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 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.
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.
// 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.
// 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.
// 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.
// ~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.
// 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.
// 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.
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.
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.
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.
// 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.
// 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 comparablerestricts 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
cmpandslicesdemonstrate idiomatic generic patterns - Zero values via
var zero Thandle 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

Go Error Handling in 2026: Patterns, Wrapping and Technical Interview Questions
Go error handling patterns and best practices: sentinel errors, custom error types, errors.Is, errors.As, error wrapping with fmt.Errorf %w, and common interview questions.

Go and gRPC in 2026: High-Performance Microservices and Interview Questions
Deep dive into gRPC with Go in 2026. Protocol Buffers, unary and streaming RPCs, interceptors, production-grade patterns, and common interview questions for backend engineers.

Go Design Patterns: Essential Patterns and Interview Questions for Go Developers
Master Go design patterns including Functional Options, Strategy, Factory, and Observer. Practical code examples, idiomatic best practices, and common interview questions for Go developers.