Go 1.24 Generic Architecture Design: 7 Production Patterns from Type Constraints to Advanced Patterns
When interface{} Meets Generics: Go's Type Safety Awakening
2 AM, production panic. Investigation reveals: a generic cache module using interface{} — the Get return value was incorrectly type-asserted, a string asserted as int, crashing at runtime. Worse, similar type assertions were scattered across 30+ files, and every data structure change required a global search-and-replace.
This isn't an isolated case. Before Go 1.18 introduced generics, interface{} and reflection were the only tools for generic programming, but runtime type safety relied entirely on developer discipline. Go 1.24 further enhances generic capabilities with more flexible type constraints and mature compiler optimizations. This article covers 7 production-grade Go generic architecture patterns to help you build type-safe, reusable, and high-performance Go services.
Key Takeaways
- Type constraints are the soul of generics: Go 1.24's type constraint syntax makes generics more than "syntactic sugar"
- Generic middleware chains: Say goodbye to
interface{}, implement type-safe HTTP middleware - Generic Repository pattern: One line for CRUD, type-safe with zero reflection
- Result type: Replace the if-else hell of error handling with generics
- Type-safe Builder: Compile-time guarantee of build completeness
- Generic concurrency patterns: Generic channels + generic worker pools
- Advanced type composition: Type-level programming with generic constraint composition
Table of Contents
- Go Generics Core Concepts Reference
- Pattern 1: Type Constraints and Interface Composition
- Pattern 2: Generic Middleware Chains
- Pattern 3: Generic Repository Pattern
- Pattern 4: Generic Error Handling and Result Type
- Pattern 5: Type-Safe Builder Pattern
- Pattern 6: Generic Concurrency Patterns
- Pattern 7: Advanced Type Composition and Type-Level Programming
- 5 Common Pitfalls and Solutions
- 10 Common Error Troubleshooting
- Advanced Optimization Tips
- Comparative Analysis: Generics vs Interfaces vs Code Generation
- Recommended Online Tools
- Summary
Go Generics Core Concepts Reference
| Concept | Syntax | Purpose | Example |
|---|---|---|---|
| Type Parameter | [T any] |
Declare generic type | func Print[T any](v T) |
| Type Constraint | [T constraints.Integer] |
Limit type parameter scope | func Sum[T Integer](s []T) T |
| Interface Constraint | interface{ Method() } |
Require specific methods | type Stringer interface{ String() string } |
| Union Type | int | float64 |
Allow multiple types | [T int | float64] |
| Approximation | ~int |
Include underlying types | [T ~int] matches type MyInt int |
| Generic Struct | type Stack[T any] struct{} |
Parameterized data structure | type Node[T any] struct{ Val T } |
| Generic Interface | type Handler[T any] interface{} |
Parameterized interface | type Store[T any] interface{ Get(id string) T } |
| Type Inference | Print(42) |
Omit type parameters | Compiler infers T = int |
Pattern 1: Type Constraints and Interface Composition
Problem: The Runtime Traps of interface{}
func Max(values []interface{}) interface{} {
if len(values) == 0 {
return nil
}
m := values[0]
for _, v := range values[1:] {
switch m.(type) {
case int:
if v.(int) > m.(int) {
m = v
}
case float64:
if v.(float64) > m.(float64) {
m = v
}
default:
panic("unsupported type")
}
}
return m
}
Runtime panics, type assertions everywhere, impossible to extend — these are the three cardinal sins of interface{}.
Solution: Type Constraints + Interface Composition
package generic
import "cmp"
type Ordered interface {
cmp.Ordered
}
func Max[T Ordered](values []T) T {
if len(values) == 0 {
var zero T
return zero
}
m := values[0]
for _, v := range values[1:] {
if v > m {
m = v
}
}
return m
}
Advanced: Custom Type Constraint Composition
package constraint
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
type Float interface {
~float32 | ~float64
}
type Integer interface {
Signed | Unsigned
}
type Numeric interface {
Integer | Float
}
func Clamp[T Numeric](value, min, max T) T {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func Abs[T Signed](v T) T {
if v < 0 {
return -v
}
return v
}
Go 1.24 Enhancement: Method Set Composition in Interface Constraints
package constraint
type Stringer interface {
String() string
}
type Validator interface {
Validate() error
}
type Entity interface {
Stringer
Validator
ID() string
}
func ProcessEntity[T Entity](entity T) error {
fmt.Println("Processing:", entity.String())
if err := entity.Validate(); err != nil {
return fmt.Errorf("validation failed for %s: %w", entity.ID(), err)
}
return nil
}
Architecture Diagram: Type Constraint Hierarchy
any
|
+--------+--------+
| |
Number String
|
+------+------+
| |
Integer Float
|
+--+--+-----+
| | |
Signed Unsigned Complex
Pattern 2: Generic Middleware Chains
Problem: The Type Hell of interface{} Middleware
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := validateToken(token)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func Handler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
fmt.Println(user.Name)
}
context.Value returns interface{}, type assertions can panic, keys can collide — classic pain points of Go HTTP middleware.
Solution: Generic Context + Type-Safe Middleware
package middleware
import (
"context"
"net/http"
)
type contextKey[T any] struct{}
func WithValue[T any](ctx context.Context, value T) context.Context {
return context.WithValue(ctx, contextKey[T]{}, value)
}
func GetValue[T any](ctx context.Context) (T, bool) {
v, ok := ctx.Value(contextKey[T]{}).(T)
return v, ok
}
type User struct {
ID string
Name string
Role string
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := validateToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := WithValue[User](r.Context(), user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := GetValue[User](r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if user.Role != role {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
Advanced: Generic Middleware Chain Framework
package middleware
import (
"net/http"
)
type Context[T any] struct {
Response http.ResponseWriter
Request *http.Request
Data T
}
type HandlerFunc[T any] func(ctx *Context[T]) error
type Middleware[T any] func(next HandlerFunc[T]) HandlerFunc[T]
func Chain[T any](handler HandlerFunc[T], middlewares ...Middleware[T]) HandlerFunc[T] {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
type APIRequest struct {
UserID string
Token string
Body []byte
}
func LoggingMiddleware[T any](next HandlerFunc[T]) HandlerFunc[T] {
return func(ctx *Context[T]) error {
fmt.Printf("[%s] %s %s\n", time.Now().Format(time.RFC3339), ctx.Request.Method, ctx.Request.URL.Path)
return next(ctx)
}
}
func RecoveryMiddleware[T any](next HandlerFunc[T]) HandlerFunc[T] {
return func(ctx *Context[T]) error {
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic recovered: %v\n", r)
http.Error(ctx.Response, "internal server error", http.StatusInternalServerError)
}
}()
return next(ctx)
}
}
func main() {
handler := func(ctx *Context[APIRequest]) error {
user, ok := GetValue[User](ctx.Request.Context())
if !ok {
http.Error(ctx.Response, "unauthorized", 401)
return nil
}
fmt.Fprintf(ctx.Response, "Hello, %s!", user.Name)
return nil
}
chained := Chain(handler, RecoveryMiddleware[APIRequest], LoggingMiddleware[APIRequest])
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
ctx := &Context[APIRequest]{
Response: w,
Request: r,
}
if err := chained(ctx); err != nil {
http.Error(w, err.Error(), 500)
}
})
}
Pattern 3: Generic Repository Pattern
Problem: CRUD Code Duplication
Each entity needs its own CRUD implementation — 5 entities means 5 nearly identical codebases, differing only in type names. Copy-paste is error-prone; modifying one and forgetting to sync the others is inevitable.
Solution: Generic Repository
package repository
import (
"context"
"database/sql"
"fmt"
)
type Entity interface {
GetID() string
SetID(id string)
}
type Repository[T Entity] struct {
db *sql.DB
tableName string
}
func NewRepository[T Entity](db *sql.DB, tableName string) *Repository[T] {
return &Repository[T]{
db: db,
tableName: tableName,
}
}
func (r *Repository[T]) GetByID(ctx context.Context, id string) (*T, error) {
query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", r.tableName)
row := r.db.QueryRowContext(ctx, query, id)
var entity T
err := row.Scan(&entity)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("entity with id %s not found", id)
}
return nil, fmt.Errorf("query failed: %w", err)
}
return &entity, nil
}
func (r *Repository[T]) Create(ctx context.Context, entity *T) error {
query := fmt.Sprintf("INSERT INTO %s (id, data) VALUES (?, ?)", r.tableName)
id := (*entity).GetID()
_, err := r.db.ExecContext(ctx, query, id, entity)
if err != nil {
return fmt.Errorf("create failed: %w", err)
}
return nil
}
func (r *Repository[T]) Delete(ctx context.Context, id string) error {
query := fmt.Sprintf("DELETE FROM %s WHERE id = ?", r.tableName)
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("delete failed: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("entity with id %s not found", id)
}
return nil
}
func (r *Repository[T]) List(ctx context.Context, offset, limit int) ([]*T, error) {
query := fmt.Sprintf("SELECT * FROM %s ORDER BY id LIMIT ? OFFSET ?", r.tableName)
rows, err := r.db.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, fmt.Errorf("list failed: %w", err)
}
defer rows.Close()
var entities []*T
for rows.Next() {
var entity T
if err := rows.Scan(&entity); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
entities = append(entities, &entity)
}
return entities, nil
}
Advanced: Generic Repository + Generic Specification
package repository
type Specification[T any] func(T) bool
func (r *Repository[T]) FindBySpec(ctx context.Context, spec Specification[T]) ([]*T, error) {
all, err := r.List(ctx, 0, 10000)
if err != nil {
return nil, err
}
var result []*T
for _, e := range all {
if spec(*e) {
result = append(result, e)
}
}
return result, nil
}
type User struct {
ID string
Name string
Email string
Age int
}
func (u User) GetID() string { return u.ID }
func (u *User) SetID(id string) { u.ID = id }
func main() {
db, _ := sql.Open("sqlite3", ":memory:")
userRepo := NewRepository[User](db, "users")
users, _ := userRepo.FindBySpec(context.Background(), func(u User) bool {
return u.Age >= 18
})
fmt.Printf("Adult users: %d\n", len(users))
}
Architecture Diagram: Generic Repository Hierarchy
┌─────────────────────────────────────────┐
│ Service Layer │
│ UserService OrderService ItemService │
├─────────────────────────────────────────┤
│ Repository[T Entity] │
│ ┌──────────┬──────────┬──────────┐ │
│ │ GetByID │ Create │ Delete │ │
│ │ List │ Update │ FindSpec │ │
│ └──────────┴──────────┴──────────┘ │
├─────────────────────────────────────────┤
│ Database Driver │
│ MySQL PostgreSQL SQLite │
└─────────────────────────────────────────┘
Pattern 4: Generic Error Handling and Result Type
Problem: The if err != nil Hell
func ProcessOrder(req OrderRequest) (*Order, error) {
user, err := userService.Get(req.UserID)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
product, err := productService.Get(req.ProductID)
if err != nil {
return nil, fmt.Errorf("get product: %w", err)
}
payment, err := paymentService.Charge(user, product.Price)
if err != nil {
return nil, fmt.Errorf("charge: %w", err)
}
order, err := orderService.Create(user, product, payment)
if err != nil {
return nil, fmt.Errorf("create order: %w", err)
}
return order, nil
}
4 operations, 4 if err != nil blocks, taking up half the code. Go generics can solve this elegantly.
Solution: Result Type
package result
import "fmt"
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 Errf[T any](format string, args ...any) Result[T] {
return Err[T](fmt.Errorf(format, args...))
}
func (r Result[T]) IsOk() bool {
return r.err == nil
}
func (r Result[T]) IsErr() bool {
return r.err != nil
}
func (r Result[T]) Unwrap() T {
if r.err != nil {
panic(fmt.Sprintf("called Unwrap on Err: %v", r.err))
}
return r.value
}
func (r Result[T]) UnwrapOr(defaultValue T) T {
if r.err != nil {
return defaultValue
}
return r.value
}
func (r Result[T]) UnwrapOrElse(fn func() T) T {
if r.err != nil {
return fn()
}
return r.value
}
func (r Result[T]) Err() error {
return r.err
}
func (r Result[T]) Value() (T, bool) {
return r.value, r.err == nil
}
func Map[T any, U any](r Result[T], fn func(T) U) Result[U] {
if r.err != nil {
return Err[U](r.err)
}
return Ok(fn(r.value))
}
func FlatMap[T any, U any](r Result[T], fn func(T) Result[U]) Result[U] {
if r.err != nil {
return Err[U](r.err)
}
return fn(r.value)
}
Refactoring ProcessOrder with Result
func ProcessOrder(req OrderRequest) Result[*Order] {
user := userService.Get(req.UserID)
if user.IsErr() {
return Errf[*Order]("get user: %w", user.Err())
}
product := productService.Get(req.ProductID)
if product.IsErr() {
return Errf[*Order]("get product: %w", product.Err())
}
payment := paymentService.Charge(user.Unwrap(), product.Unwrap().Price)
if payment.IsErr() {
return Errf[*Order]("charge: %w", payment.Err())
}
order := orderService.Create(user.Unwrap(), product.Unwrap(), payment.Unwrap())
if order.IsErr() {
return Errf[*Order]("create order: %w", order.Err())
}
return order
}
Advanced: Chained Result Operations
func ProcessOrderChain(req OrderRequest) Result[*Order] {
return FlatMap(
userService.Get(req.UserID),
func(user *User) Result[*Order] {
return FlatMap(
productService.Get(req.ProductID),
func(product *Product) Result[*Order] {
return FlatMap(
paymentService.Charge(user, product.Price),
func(payment *Payment) Result[*Order] {
return orderService.Create(user, product, payment)
},
)
},
)
},
)
}
Result vs error Comparison
| Feature | error |
Result[T] |
|---|---|---|
| Type Safety | No, requires manual assertion | Yes, compile-time guarantee |
| Chaining | Not supported | Map/FlatMap |
| Default values | Manual handling | UnwrapOr |
| Forced checking | No, can be ignored | IsOk/IsErr |
| Performance overhead | Zero | Minimal (one extra struct) |
| Ecosystem compatibility | Native | Requires adapter layer |
Pattern 5: Type-Safe Builder Pattern
Problem: Optional Parameters and Config Validation
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
MaxConns int
TLS bool
CertFile string
KeyFile string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
func NewServerConfig(host string, port int, timeout time.Duration, maxConns int, tls bool, certFile string, keyFile string, readTimeout time.Duration, writeTimeout time.Duration) *ServerConfig {
return &ServerConfig{
Host: host, Port: port, Timeout: timeout,
MaxConns: maxConns, TLS: tls, CertFile: certFile,
KeyFile: keyFile, ReadTimeout: readTimeout, WriteTimeout: writeTimeout,
}
}
A 9-parameter constructor — impossible to tell which argument is which at the call site. If TLS is enabled but cert paths are forgotten, the error only surfaces at runtime.
Solution: Generic Builder
package builder
type Builder[T any] struct {
target *T
errors []error
}
func NewBuilder[T any]() *Builder[T] {
return &Builder[T]{
target: new(T),
}
}
func (b *Builder[T]) With(fn func(*T)) *Builder[T] {
fn(b.target)
return b
}
func (b *Builder[T]) WithValidate(fn func(*T) error) *Builder[T] {
if err := fn(b.target); err != nil {
b.errors = append(b.errors, err)
}
return b
}
func (b *Builder[T]) Build() (*T, error) {
if len(b.errors) > 0 {
return nil, fmt.Errorf("build failed: %v", b.errors)
}
return b.target, nil
}
Using the Generic Builder for ServerConfig
func main() {
config, err := NewBuilder[ServerConfig]().
With(func(c *ServerConfig) {
c.Host = "0.0.0.0"
c.Port = 8080
c.Timeout = 30 * time.Second
c.MaxConns = 1000
c.TLS = true
}).
WithValidate(func(c *ServerConfig) error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.TLS && c.CertFile == "" {
return fmt.Errorf("TLS enabled but no cert file")
}
if c.TLS && c.KeyFile == "" {
return fmt.Errorf("TLS enabled but no key file")
}
return nil
}).
Build()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Config: %+v\n", config)
}
Advanced: Type-State Builder (Compile-Time Required Fields)
package builder
type ServerConfigUnset struct{}
type ServerConfigSet struct {
Host string
Port int
}
type TypedBuilder[State any] struct {
host string
port int
timeout time.Duration
maxConns int
tls bool
certFile string
keyFile string
}
func NewTypedBuilder() *TypedBuilder[ServerConfigUnset] {
return &TypedBuilder[ServerConfigUnset]{}
}
func (b *TypedBuilder[ServerConfigUnset]) HostPort(host string, port int) *TypedBuilder[ServerConfigSet] {
b.host = host
b.port = port
return (*TypedBuilder[ServerConfigSet])(unsafe.Pointer(b))
}
func (b *TypedBuilder[ServerConfigSet]) Timeout(d time.Duration) *TypedBuilder[ServerConfigSet] {
b.timeout = d
return b
}
func (b *TypedBuilder[ServerConfigSet]) TLS(certFile, keyFile string) *TypedBuilder[ServerConfigSet] {
b.tls = true
b.certFile = certFile
b.keyFile = keyFile
return b
}
func (b *TypedBuilder[ServerConfigSet]) Build() *ServerConfig {
return &ServerConfig{
Host: b.host, Port: b.port, Timeout: b.timeout,
MaxConns: b.maxConns, TLS: b.tls,
CertFile: b.certFile, KeyFile: b.keyFile,
}
}
At the call site, you can't call Build without first calling HostPort — compile-time enforcement of required fields.
Pattern 6: Generic Concurrency Patterns
Problem: Type-Unsafe Concurrency Primitives
func FanIn(channels ...chan interface{}) chan interface{} {
out := make(chan interface{})
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c chan interface{}) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
chan interface{} — the same old runtime type safety problem.
Solution: Generic Concurrency Primitives
package concurrent
import "sync"
func FanIn[T any](channels ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan T) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func FanOut[T any, R any](input <-chan T, worker func(T) R, count int) <-chan R {
out := make(chan R)
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for v := range input {
out <- worker(v)
}
}()
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func Pipeline[T any, R any](input <-chan T, stage func(T) R) <-chan R {
out := make(chan R)
go func() {
defer close(out)
for v := range input {
out <- stage(v)
}
}()
return out
}
Generic Worker Pool
package concurrent
import "context"
type Job[T any, R any] struct {
Input T
Result chan<- Result[R]
}
type Pool[T any, R any] struct {
jobs chan Job[T, R]
worker func(context.Context, T) (R, error)
count int
ctx context.Context
cancel context.CancelFunc
}
func NewPool[T any, R any](ctx context.Context, worker func(context.Context, T) (R, error), count int) *Pool[T, R] {
ctx, cancel := context.WithCancel(ctx)
p := &Pool[T, R]{
jobs: make(chan Job[T, R], count*2),
worker: worker,
count: count,
ctx: ctx,
cancel: cancel,
}
p.start()
return p
}
func (p *Pool[T, R]) start() {
for i := 0; i < p.count; i++ {
go func() {
for job := range p.jobs {
select {
case <-p.ctx.Done():
job.Result <- Err[R](p.ctx.Err())
return
default:
value, err := p.worker(p.ctx, job.Input)
if err != nil {
job.Result <- Err[R](err)
} else {
job.Result <- Ok(value)
}
}
}
}()
}
}
func (p *Pool[T, R]) Submit(input T) Result[R] {
resultCh := make(chan Result[R], 1)
select {
case p.jobs <- Job[T, R]{Input: input, Result: resultCh}:
case <-p.ctx.Done():
return Err[R](p.ctx.Err())
}
return <-resultCh
}
func (p *Pool[T, R]) Shutdown() {
p.cancel()
close(p.jobs)
}
Using the Generic Worker Pool
func main() {
ctx := context.Background()
pool := NewPool[string, int](ctx, func(ctx context.Context, url string) (int, error) {
resp, err := http.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}, 10)
urls := []string{
"https://example.com",
"https://httpbin.org/status/200",
"https://httpbin.org/status/404",
}
for _, url := range urls {
result := pool.Submit(url)
if result.IsOk() {
fmt.Printf("%s -> %d\n", url, result.Unwrap())
} else {
fmt.Printf("%s -> error: %v\n", url, result.Err())
}
}
pool.Shutdown()
}
Pattern 7: Advanced Type Composition and Type-Level Programming
Problem: Complex Type Relationships Are Hard to Express
When your domain model has complex type dependencies — "an order contains a user and a product, a product has a price, a price is a numeric type" — interface{} can't express these constraints at compile time.
Solution: Generic Type Composition
package domain
type Identifiable interface {
~string | ~int | ~int64
}
type Timestamped interface {
GetCreatedAt() time.Time
GetUpdatedAt() time.Time
}
type SoftDeletable interface {
GetDeletedAt() *time.Time
IsDeleted() bool
}
type BaseEntity[ID Identifiable] struct {
ID ID
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
func (b BaseEntity[ID]) GetCreatedAt() time.Time { return b.CreatedAt }
func (b BaseEntity[ID]) GetUpdatedAt() time.Time { return b.UpdatedAt }
func (b BaseEntity[ID]) GetDeletedAt() *time.Time { return b.DeletedAt }
func (b BaseEntity[ID]) IsDeleted() bool { return b.DeletedAt != nil }
Generic Event System
package event
type Event interface {
EventName() string
OccurredAt() time.Time
}
type Handler[T Event] func(ctx context.Context, event T) error
type Bus struct {
handlers map[string][]any
mu sync.RWMutex
}
func NewBus() *Bus {
return &Bus{
handlers: make(map[string][]any),
}
}
func Subscribe[T Event](bus *Bus, handler Handler[T]) {
bus.mu.Lock()
defer bus.mu.Unlock()
var zero T
name := zero.EventName()
bus.handlers[name] = append(bus.handlers[name], handler)
}
func Publish[T Event](bus *Bus, ctx context.Context, event T) error {
bus.mu.RLock()
handlers := bus.handlers[event.EventName()]
bus.mu.RUnlock()
var g errgroup.Group
for _, h := range handlers {
handler := h.(Handler[T])
g.Go(func() error {
return handler(ctx, event)
})
}
return g.Wait()
}
type UserCreatedEvent struct {
UserID string
UserName string
Timestamp time.Time
}
func (e UserCreatedEvent) EventName() string { return "user.created" }
func (e UserCreatedEvent) OccurredAt() time.Time { return e.Timestamp }
type OrderPlacedEvent struct {
OrderID string
UserID string
Amount float64
Timestamp time.Time
}
func (e OrderPlacedEvent) EventName() string { return "order.placed" }
func (e OrderPlacedEvent) OccurredAt() time.Time { return e.Timestamp }
Generic Dependency Injection Container
package di
type Provider[T any] func(container *Container) (T, error)
type Container struct {
providers map[reflect.Type]any
instances map[reflect.Type]any
mu sync.RWMutex
}
func NewContainer() *Container {
return &Container{
providers: make(map[reflect.Type]any),
instances: make(map[reflect.Type]any),
}
}
func Provide[T any](c *Container, provider Provider[T]) {
c.mu.Lock()
defer c.mu.Unlock()
var zero T
typ := reflect.TypeOf(zero)
c.providers[typ] = provider
}
func Resolve[T any](c *Container) (T, error) {
c.mu.RLock()
var zero T
typ := reflect.TypeOf(zero)
if inst, ok := c.instances[typ]; ok {
c.mu.RUnlock()
return inst.(T), nil
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
provider, ok := c.providers[typ]
if !ok {
return zero, fmt.Errorf("no provider for type %T", zero)
}
instance, err := provider.(Provider[T])(c)
if err != nil {
return zero, fmt.Errorf("provider failed: %w", err)
}
c.instances[typ] = instance
return instance, nil
}
Architecture Diagram: Generic Type Composition
┌──────────────────────────────────────────────┐
│ Domain Layer │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ BaseEntity │ │ Event │ │
│ │ [ID Identif.]│ │ [T Event] │ │
│ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌─────────┴────────────┐ │
│ │ User │ │ UserCreatedEvent │ │
│ │ Order │ │ OrderPlacedEvent │ │
│ │ Product │ │ PaymentCompletedEvt │ │
│ └──────────────┘ └──────────────────────┘ │
├──────────────────────────────────────────────┤
│ Infrastructure Layer │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Repository │ │ EventBus │ │
│ │ [T Entity] │ │ [T Event] │ │
│ └──────────────┘ └──────────────────────┘ │
├──────────────────────────────────────────────┤
│ Application Layer │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ DI Container │ │ Middleware Chain │ │
│ │ [T any] │ │ [T any] │ │
│ └──────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────┘
5 Common Pitfalls and Solutions
Pitfall 1: Generic Methods Don't Support Type Inference
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
var s Stack[int]
s.Push(42)
func Push[T any](s *Stack[T], v T) { s.items = append(s.items, v) }
Push(&s, 42)
In Go 1.24, method calls don't require explicit type parameters, but generic functions sometimes do.
Pitfall 2: Generic Structs Can't Have Method Type Parameters
type Converter[T any] struct{}
func (c *Converter[T]) Convert[R any](v T) R {
}
Compile error: Go doesn't support additional type parameters on methods. Solution: use package-level functions.
func Convert[T any, R any](v T, fn func(T) R) R {
return fn(v)
}
Pitfall 3: Generics and Interface Assertions Are Incompatible
var _ json.Marshaler = Stack[int]{}
You can't make a generic type uniformly implement an interface because Stack[int] and Stack[string] are different types. Solution: implement for each concrete instantiation separately.
Pitfall 4: Generic Zero Value Trap
func First[T any](s []T) T {
if len(s) == 0 {
var zero T
return zero
}
return s[0]
}
The zero value of T might be nil (if T is a pointer/interface/slice/map/channel). Callers need to check. Go 1.24 has no nullable constraint — handle this yourself.
Pitfall 5: Generics and Embedding Are Incompatible
type GenericEmbed[T any] struct {
T
Name string
}
Go doesn't support embedding generic type parameters. Solution: use composition instead of embedding.
type GenericHolder[T any] struct {
Value T
Name string
}
10 Common Error Troubleshooting
| Error | Cause | Solution |
|---|---|---|
cannot use generic type without instantiation |
Using uninstantiated generic type | Specify type parameter: Stack[int] |
interface contains type constraints |
Union type used outside constraints | Union types only belong in type constraints |
invalid recursive type: T refers to itself |
Generic type recursively references itself | Redesign type relationships to avoid direct recursion |
method must have no type parameters |
Method defined with extra type parameters | Use package-level generic functions instead |
cannot use ~T in interface |
Approximation used in regular interface | Approximation only works in type constraint interfaces |
T does not implement constraint |
Type parameter doesn't satisfy constraint | Check constraint conditions, ensure type implements required methods |
implicit assignment of unexported field |
Accessing private fields across packages | Export fields or provide public methods |
type parameter T is not used |
Declared but unused type parameter | Remove unused type parameters |
panic: called Unwrap on Err |
Called Unwrap on an Err Result | Check with IsOk() first, or use UnwrapOr |
cannot range over T |
Attempting to range over a type parameter | Add []T constraint or use a concrete type |
Advanced Optimization Tips
Tip 1: Monomorphization Optimization for Generic Code
The Go compiler performs monomorphization on generic code, generating specialized code for each concrete type. For value types (int, float, etc.), this brings significant performance gains by avoiding boxing/unboxing.
func Sum[T Integer](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
After compilation, Sum[int] and Sum[int64] are two independent code paths, each optimized for their respective type.
Tip 2: Shape Optimization to Reduce Code Bloat
For pointer types and interface types, Go uses dictionary passing rather than full monomorphization to reduce code bloat:
func Process[T Stringer](v T) string {
return v.String()
}
Process[*MyType] and Process[*YourType] may share the same code, differing only in their dictionaries.
Tip 3: Generics and Inlining Optimization
Short generic functions are more likely to be inlined:
func Map[T any, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}
Keep generic functions short so the compiler can inline them, avoiding function call overhead.
Tip 4: Generic Cache Pattern
package cache
type Cache[T any] struct {
data map[string]T
mu sync.RWMutex
}
func NewCache[T any]() *Cache[T] {
return &Cache[T]{
data: make(map[string]T),
}
}
func (c *Cache[T]) Get(key string) (T, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache[T]) Set(key string, value T) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
Generic caching avoids the type assertion overhead of interface{} while maintaining generality.
Comparative Analysis: Generics vs Interfaces vs Code Generation
| Dimension | Generics | Interfaces | Code Generation |
|---|---|---|---|
| Type Safety | Compile-time | Runtime | Compile-time |
| Performance | Monomorphization | Virtual call overhead | Zero overhead |
| Code Volume | Low | Medium | High (generated) |
| Debugging Difficulty | Medium | Low | Low |
| IDE Support | Go 1.24 mature | Excellent | Depends on generator |
| Learning Curve | Steep | Gentle | Medium |
| Use Cases | Generic algorithms, data structures | Polymorphism, decoupling | Batch CRUD |
| Binary Size | May increase | Small | Significantly larger |
| Compile Speed | Slightly slower | Fast | Fast (extra generation step) |
Decision Tree
Need type safety?
├── No → Interfaces
└── Yes
├── Need generic algorithms/data structures?
│ └── Yes → Generics
└── No
├── Fixed, small number of types?
│ └── Yes → Interfaces + type assertions
└── No → Code generation
Recommended Online Tools
- JSON Formatter — Format JSON output from generic structs
- Code Formatter — Format Go generic code
- Regex Cheatsheet — Handle text replacement in Go generic code
External References
- Go Generics Official Proposal — Official Go generics design document
- Go 1.24 Release Notes — Go 1.24 generics-related improvements
Summary
Go 1.24 generic architecture design has evolved from "experimental" to "production-ready." The 7 core patterns cover the complete chain from type constraints to advanced composition:
- Type Constraints and Interface Composition — The foundation of generics, replacing runtime assertions with constraints
- Generic Middleware Chains — Say goodbye to type-unsafe
context.Value - Generic Repository Pattern — One line for type-safe CRUD
- Generic Error Handling and Result Type — Chain operations replacing
if err != nilhell - Type-Safe Builder Pattern — Compile-time guarantee of build completeness
- Generic Concurrency Patterns — Type-safe FanIn/FanOut/Worker Pool
- Advanced Type Composition and Type-Level Programming — Generic event systems, DI containers
Go generics aren't a silver bullet, but they offer a better balance between type safety, code reuse, and performance. Whether to choose generics, interfaces, or code generation depends on your specific scenario — refer to the decision tree above.
Related Reading:
Try these browser-local tools — no sign-up required →