Go Generics Tutorial
Generics (introduced in Go 1.18) enable writing flexible, type-safe code without sacrificing performance. This tutorial covers type parameters, constraints, and practical use cases.
1. Basic Generics
1.1 Type Parameters
Generic functions declare type parameters in square brackets:
package main
import "fmt"
// T is a type parameter
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
func main() {
PrintSlice([]int{1, 2, 3}) // Works with int
PrintSlice([]string{"a", "b"}) // Works with string
}
Key Syntax:
- [T any]: Declares type parameter T (accepts any type)
- any: Built-in constraint for all types
- Type inference: Compiler deduces types automatically
2. Constraints
2.1 Built-in Constraints
Predefined constraints limit allowable types:
// Comparable constraint allows == and !=
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
// Ordered constraint allows <, >, etc.
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
2.2 Custom Constraints
Define interfaces as type constraints:
type Number interface {
int | float64 | float32
}
func Sum[T Number](numbers []T) T {
var total T
for _, n := range numbers {
total += n
}
return total
}
3. Generic Data Structures
3.1 Generic Types
Create type-safe container types:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() T {
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}
func main() {
intStack := Stack[int]{}
intStack.Push(42)
fmt.Println(intStack.Pop())
}
3.2 Type-Safe Collections
Implement common algorithms generically:
func Map[T, U any](input []T, f func(T) U) []U {
result := make([]U, len(input))
for i, v := range input {
result[i] = f(v)
}
return result
}
func main() {
doubled := Map([]int{1, 2, 3}, func(x int) int {
return x * 2
})
fmt.Println(doubled) // [2 4 6]
}
4. Advanced Patterns
4.1 Multiple Type Parameters
Functions can accept multiple generic types:
func Zip[K comparable, V any](keys []K, values []V) map[K]V {
result := make(map[K]V)
for i, key := range keys {
result[key] = values[i]
}
return result
}
func main() {
m := Zip(
[]string{"a", "b"},
[]int{1, 2},
)
fmt.Println(m) // map[a:1 b:2]
}
4.2 Generic Interfaces
Interfaces can be parameterized:
type Store[T any] interface {
Get(id string) (T, error)
Put(id string, value T) error
}
type MemoryStore[T any] struct {
data map[string]T
}
func (m *MemoryStore[T]) Get(id string) (T, error) {
// Implementation...
}
func (m *MemoryStore[T]) Put(id string, value T) error {
// Implementation...
}
5. Best Practices
5.1 When to Use Generics
- Good for: Container types, algorithms, math operations
- Avoid when: Interface-based polymorphism would work
5.2 Performance Considerations
// Generic functions are compiled to concrete implementations
// No runtime overhead compared to non-generic code
// But beware of code bloat from many instantiations
// Keep generic functions small and focused
5.3 Error Handling
// Constraints can include error types
type Parseable interface {
~string | ~[]byte
Parse() (T, error)
}
func ParseInput[T Parseable](input T) (T, error) {
return input.Parse()
}
×