Skip to content

🔷 Generics

"Generics: Write once, use with any type."
Go 1.18 mang Generics đến Go sau 13 năm thiết kế cẩn thận.

📐 Type Parameters Basics

Basic Syntax

go
// Generic function
func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Usage - type inference
result := Min(3, 5)           // T inferred as int
result2 := Min("a", "b")      // T inferred as string

// Explicit type
result3 := Min[float64](3.14, 2.71)

Generic Types

go
// Generic struct
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, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Usage
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop()  // 2

stringStack := Stack[string]{}
stringStack.Push("hello")

🔒 Constraints

Built-in Constraints

go
import "cmp"

// any - allows any type
func PrintAnything[T any](v T) {
    fmt.Println(v)
}

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

// cmp.Ordered - types that support < > <= >=
func Max[T cmp.Ordered](values ...T) T {
    if len(values) == 0 {
        var zero T
        return zero
    }
    max := values[0]
    for _, v := range values[1:] {
        if v > max {
            max = v
        }
    }
    return max
}

Custom Constraints

go
// Interface constraint
type Stringer interface {
    String() string
}

func Join[T Stringer](items []T, sep string) string {
    var parts []string
    for _, item := range items {
        parts = append(parts, item.String())
    }
    return strings.Join(parts, sep)
}

// Type set constraint (union)
type Number interface {
    int | int32 | int64 | float32 | float64
}

func Sum[T Number](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

// Underlying type constraint (~)
type Integer interface {
    ~int | ~int32 | ~int64
}

type UserID int64  // Custom type with underlying int64

func Double[T Integer](v T) T {
    return v * 2
}

// Works with custom types!
var id UserID = 5
doubled := Double(id)  // UserID(10)

Combining Constraints

go
// Multiple constraints
type OrderedStringer interface {
    cmp.Ordered
    fmt.Stringer
}

// Struct constraint (field access)
type HasID interface {
    ~struct{ ID int64 } | ~struct{ ID int64; Name string }
}
// Note: This is very limited - prefer method constraints

🏗️ Generic Data Structures

Generic Result Type

go
// Result type for error handling
type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](value T) Result[T] {
    return Result[T]{value: value}
}

func Err[T any](err error) Result[T] {
    return Result[T]{err: err}
}

func (r Result[T]) Unwrap() (T, error) {
    return r.value, r.err
}

func (r Result[T]) UnwrapOr(defaultValue T) T {
    if r.err != nil {
        return defaultValue
    }
    return r.value
}

func (r Result[T]) Map(fn func(T) T) Result[T] {
    if r.err != nil {
        return r
    }
    return Ok(fn(r.value))
}

// Usage
result := Ok(42).Map(func(n int) int { return n * 2 })
value := result.UnwrapOr(0)  // 84

Generic Cache

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

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

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

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

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

// Usage
userCache := NewCache[int64, *User](5 * time.Minute)
userCache.Set(1, &User{ID: 1, Name: "Alice"})
user, found := userCache.Get(1)

Generic Slice Utilities

go
// Map function
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Filter function
func Filter[T any](slice []T, predicate func(T) bool) []T {
    result := make([]T, 0)
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce function
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5}

doubled := Map(numbers, func(n int) int { return n * 2 })
// [2, 4, 6, 8, 10]

evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
// [2, 4]

sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })
// 15

⚖️ When to Use Generics

Good Use Cases

go
// 1. Container types
type Set[T comparable] map[T]struct{}

// 2. Utility functions
func Keys[K comparable, V any](m map[K]V) []K

// 3. Type-safe wrappers
type Optional[T any] struct { /* ... */ }

// 4. Algorithm implementations
func Sort[T cmp.Ordered](slice []T)

When NOT to Use Generics

go
// 1. When interface{} / any works fine
func PrintValue(v any) { fmt.Println(v) }

// 2. When you have single concrete type
func ProcessUser(u *User) error  // NOT ProcessUser[T *User]

// 3. When implementation differs per type
// Use interface instead
type Formatter interface {
    Format() string
}

Decision Flow

Do you need compile-time type safety?
├── NO → Use interface{} / any
└── YES

    Does logic differ per type?
    ├── YES → Use interface with methods
    └── NO

        Is it a container/utility?
        ├── YES → Use Generics ✅
        └── NO → Probably don't need generics

💻 Engineering Example: Generic Repository

go
// Generic repository pattern
type Repository[T any, ID comparable] interface {
    GetByID(ctx context.Context, id ID) (*T, error)
    List(ctx context.Context, limit, offset int) ([]T, error)
    Create(ctx context.Context, entity *T) error
    Update(ctx context.Context, entity *T) error
    Delete(ctx context.Context, id ID) error
}

// Base implementation
type BaseRepository[T any, ID comparable] struct {
    db        *sql.DB
    tableName string
}

func (r *BaseRepository[T, ID]) GetByID(ctx context.Context, id ID) (*T, error) {
    query := fmt.Sprintf("SELECT * FROM %s WHERE id = $1", r.tableName)
    
    row := r.db.QueryRowContext(ctx, query, id)
    
    var entity T
    if err := scanRow(row, &entity); err != nil {
        return nil, err
    }
    return &entity, nil
}

// Concrete implementations
type UserRepository struct {
    *BaseRepository[User, int64]
}

func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{
        BaseRepository: &BaseRepository[User, int64]{
            db:        db,
            tableName: "users",
        },
    }
}

// Add user-specific methods
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
    // Custom implementation
}

Ship-to-Prod Checklist

Code Quality

  • [ ] Named type parameters (T, K, V are OK; Element better than E)
  • [ ] Constraint documented in function comments
  • [ ] Zero value handling considered
  • [ ] Performance benchmarked vs non-generic version

Design

  • [ ] Generics justified (not over-engineered)
  • [ ] Interface preferred when behavior differs per type
  • [ ] Type inference works in common cases
  • [ ] Error messages readable when constraint violated

Testing

  • [ ] Multiple types tested to ensure generic correctness
  • [ ] Edge cases (nil, zero values) tested
  • [ ] Benchmarks comparing generic vs specialized

📊 Summary

ConceptSyntaxUse Case
Type Parameter[T any]Any single type
Constraint[T Stringer]Types with methods
Type Union[T int | string]Specific types
Underlying[T ~int]Custom types
Multiple Params[K, V any]Maps, pairs

➡️ Tiếp theo

Generics nắm vững rồi! Tiếp theo: Reflection - Runtime type inspection.