Loading...
Loading...

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()
}
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home