Giao diện
🔷 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.
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) // 84Generic 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
| Concept | Syntax | Use 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.