Go Generics
Generics in Go
Generics were introduced in Go 1.18 (released March 2022) and represent one of the most significant additions to the language since its inception.
Core Concepts
Type Parameters
Type parameters allow functions and types to work with any type that satisfies certain constraints:
1func Print[T any](value T) {
2 fmt.Println(value)
3}
4
5// Usage
6Print[int](42)
7Print[string]("hello")
8Print(3.14) // Type inference: T inferred as float64
Type Constraints
Constraints specify what operations are permitted on a type parameter. They are defined using interfaces:
1// Built-in constraint: any (alias for interface{})
2func Identity[T any](v T) T {
3 return v
4}
5
6// Built-in constraint: comparable (supports == and !=)
7func Contains[T comparable](slice []T, target T) bool {
8 for _, v := range slice {
9 if v == target {
10 return true
11 }
12 }
13 return false
14}Custom Constraints
You can define your own constraints using interface syntax:
1// Method-based constraint
2type Stringer interface {
3 String() string
4}
5
6func Stringify[T Stringer](v T) string {
7 return v.String()
8}
9
10// Type set constraint (union of types)
11type Number interface {
12 int | int8 | int16 | int32 | int64 |
13 uint | uint8 | uint16 | uint32 | uint64 |
14 float32 | float64
15}
16
17func Sum[T Number](values []T) T {
18 var total T
19 for _, v := range values {
20 total += v
21 }
22 return total
23}The ~ Operator (Underlying Type)
The tilde operator matches types with the same underlying type:
1type MyInt int
2
3type Integer interface {
4 ~int | ~int64 // Matches int, int64, AND any type with int or int64 as underlying type
5}
6
7func Double[T Integer](v T) T {
8 return v * 2
9}
10
11var x MyInt = 5
12Double(x) // Works because MyInt's underlying type is int
Combining Constraints
You can combine method requirements with type sets:
1type OrderedStringer interface {
2 ~int | ~string
3 String() string
4}Generic Types
Generic Structs
1type Stack[T any] struct {
2 items []T
3}
4
5func (s *Stack[T]) Push(item T) {
6 s.items = append(s.items, item)
7}
8
9func (s *Stack[T]) Pop() (T, bool) {
10 if len(s.items) == 0 {
11 var zero T
12 return zero, false
13 }
14 item := s.items[len(s.items)-1]
15 s.items = s.items[:len(s.items)-1]
16 return item, true
17}
18
19// Usage
20intStack := Stack[int]{}
21intStack.Push(1)
22intStack.Push(2)Generic Maps and Slices
1type Set[T comparable] map[T]struct{}
2
3func NewSet[T comparable]() Set[T] {
4 return make(Set[T])
5}
6
7func (s Set[T]) Add(v T) {
8 s[v] = struct{}{}
9}
10
11func (s Set[T]) Contains(v T) bool {
12 _, ok := s[v]
13 return ok
14}The constraints Package
The golang.org/x/exp/constraints package provides useful predefined constraints:
1import "golang.org/x/exp/constraints"
2
3// constraints.Ordered: types that support < > <= >=
4func Max[T constraints.Ordered](a, b T) T {
5 if a > b {
6 return a
7 }
8 return b
9}
10
11// constraints.Integer: all integer types
12// constraints.Float: all float types
13// constraints.Complex: all complex types
14// constraints.Signed: all signed integer types
15// constraints.Unsigned: all unsigned integer types
Multiple Type Parameters
1type Pair[K comparable, V any] struct {
2 Key K
3 Value V
4}
5
6func NewPair[K comparable, V any](k K, v V) Pair[K, V] {
7 return Pair[K, V]{Key: k, Value: v}
8}
9
10// Map function with two type parameters
11func Map[T, U any](slice []T, f func(T) U) []U {
12 result := make([]U, len(slice))
13 for i, v := range slice {
14 result[i] = f(v)
15 }
16 return result
17}
18
19// Usage
20nums := []int{1, 2, 3}
21strs := Map(nums, func(n int) string {
22 return fmt.Sprintf("%d", n)
23})Type Inference
Go can often infer type parameters from arguments:
1func First[T any](slice []T) T {
2 return slice[0]
3}
4
5// Explicit
6result := First[int]([]int{1, 2, 3})
7
8// Inferred (preferred when unambiguous)
9result := First([]int{1, 2, 3})Limitations and Gotchas
1. No Method Type Parameters
Methods cannot have their own type parameters (only the receiver can be generic):
1type Container[T any] struct { value T }
2
3// ❌ Invalid: methods cannot have additional type parameters
4// func (c Container[T]) Convert[U any]() U { ... }
5
6// ✅ Valid: use a standalone function instead
7func Convert[T, U any](c Container[T], f func(T) U) U {
8 return f(c.value)
9}2. No Specialization
You cannot provide specialized implementations for specific types:
1// ❌ Not possible in Go
2func Print[T any](v T) { fmt.Println(v) }
3func Print[int](v int) { fmt.Printf("Integer: %d\n", v) } // Cannot specialize
3. Zero Values
Getting the zero value of a generic type:
1func Zero[T any]() T {
2 var zero T
3 return zero
4}
5
6// Or using *new(T)
7func Zero2[T any]() T {
8 return *new(T)
9}4. Type Assertions with Generics
1func Process[T any](v T) {
2 // Type switch works
3 switch val := any(v).(type) {
4 case int:
5 fmt.Println("int:", val)
6 case string:
7 fmt.Println("string:", val)
8 default:
9 fmt.Println("other:", val)
10 }
11}5. Pointer Constraints
A common pattern for methods that require a pointer receiver:
1type Setter interface {
2 Set(string)
3}
4
5// Constraint: *T must implement Setter
6func SetAll[T any, PT interface { *T; Setter }](items []T, value string) {
7 for i := range items {
8 PT(&items[i]).Set(value)
9 }
10}Practical Patterns
Generic Result Type
1type Result[T any] struct {
2 Value T
3 Err error
4}
5
6func Ok[T any](v T) Result[T] {
7 return Result[T]{Value: v}
8}
9
10func Err[T any](err error) Result[T] {
11 return Result[T]{Err: err}
12}Generic Optional Type
1type Optional[T any] struct {
2 value *T
3}
4
5func Some[T any](v T) Optional[T] {
6 return Optional[T]{value: &v}
7}
8
9func None[T any]() Optional[T] {
10 return Optional[T]{}
11}
12
13func (o Optional[T]) IsSome() bool {
14 return o.value != nil
15}
16
17func (o Optional[T]) Unwrap() T {
18 if o.value == nil {
19 panic("called Unwrap on None")
20 }
21 return *o.value
22}Generic Cache
1type Cache[K comparable, V any] struct {
2 mu sync.RWMutex
3 items map[K]V
4}
5
6func NewCache[K comparable, V any]() *Cache[K, V] {
7 return &Cache[K, V]{items: make(map[K]V)}
8}
9
10func (c *Cache[K, V]) Get(key K) (V, bool) {
11 c.mu.RLock()
12 defer c.mu.RUnlock()
13 v, ok := c.items[key]
14 return v, ok
15}
16
17func (c *Cache[K, V]) Set(key K, value V) {
18 c.mu.Lock()
19 defer c.mu.Unlock()
20 c.items[key] = value
21}Performance Considerations
Go generics use a hybrid implementation:
- GCShape stenciling: Functions are compiled once per “GC shape” (types with the same memory layout share implementations)
- Dictionary passing: Runtime type information is passed via hidden dictionary parameters
This means:
- Generic code has minimal runtime overhead in most cases
- Very hot loops with primitives may benefit from non-generic specialized implementations
- Interface-based polymorphism and generics have similar performance characteristics
#golang #programming #generics #syntax #coding #software #engineering #computer