Go Language Features Chapter 04

Go features beyond concurrency, context, interfaces, and error handling (covered in chapters 01–03). This page catalogs every distinctive Go language feature used across the 1Engage backend — generics, struct embedding, reflection, defer, closures, type assertions, receivers, function types, init functions, and build-time injection — with real code, file paths, and rationale.

4.1 Generics (Type Parameters)

Generics Type Parameters

WHAT

Go 1.18+ type parameters allow writing functions and structs that work with any type satisfying a constraint. The codebase uses the any constraint (alias for interface{}) for generic HTTP response wrappers and utility helpers.

WHY

Without generics, the codebase would need separate response functions for every data type (SendUserJSON, SendTenantJSON, SendCampaignJSON, etc.) or use interface{} everywhere and lose type safety. Generics eliminate this duplication while keeping full compile-time type checking.

HOW

Generic Response Structs

pkg/shared/http/response.go
type Response[T any] struct {
    Success bool `json:"success"`
    Data    T    `json:"data"`
    Meta    Meta `json:"meta"`
}

type PaginatedResponse[T any] struct {
    Success    bool       `json:"success"`
    Data       []T        `json:"data"`
    Pagination Pagination `json:"pagination"`
    Meta       Meta       `json:"meta"`
}
One struct, every handler. Response[User], Response[Tenant], PaginatedResponse[Campaign] — the type parameter T is inferred at each call site. JSON serialization works correctly because encoding/json inspects the concrete type at runtime.

Generic Helper Functions

pkg/shared/http/response.go
func SendJSON[T any](w http.ResponseWriter, r *http.Request, status int, data T) {
    resp := Response[T]{
        Success: status >= 200 && status < 300,
        Data:    data,
        Meta:    buildMeta(r),
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(resp)
}

func SendPaginated[T any](w http.ResponseWriter, r *http.Request, data []T, page, limit, total int) {
    resp := PaginatedResponse[T]{
        Success:    true,
        Data:       data,
        Pagination: buildPagination(page, limit, total),
        Meta:       buildMeta(r),
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(resp)
}

Zero-Value Generic Utility

broadcast-service/internal/handler/recipient_handler.go
func valueOrEmpty[T any](ptr *T) T {
    if ptr == nil {
        var zero T
        return zero   // returns "" for string, 0 for int, etc.
    }
    return *ptr
}
Pattern: var zero T is Go's idiomatic way to get the zero value for any type parameter. This replaces the need for separate stringOrEmpty(), intOrZero() functions for each nullable field type.

4.2 Struct Embedding (Composition over Inheritance)

Embedding Composition

WHAT

Go has no inheritance. Instead, it provides struct embedding — placing one type inside another without a field name. The embedded type's methods are promoted to the outer type, and the outer type automatically satisfies any interface the embedded type implements.

WHY

Embedding enables code reuse without inheritance hierarchies. It's used throughout the codebase for interface composition, combining capabilities (producer + consumer), sharing model fields (base tenant + create/update variants), and promoting methods to satisfy interfaces automatically.

HOW

Interface Composition

Smaller interfaces are composed into larger ones via embedding:

type Producer interface {
    Publish(ctx context.Context, topic string, event *Event) error
    Close() error
}

type Consumer interface {
    Subscribe(topic, group string, handler Handler)
    Run(ctx context.Context) error
}

// Composed interface: embeds both Producer and Consumer
type ProducerConsumer interface {
    Producer
    Consumer
}
Interface segregation. Handlers that only publish depend on Producer. Workers that only consume depend on Consumer. Services that do both accept ProducerConsumer. No type is forced to implement unused methods.

Struct Composition (Method Promotion)

The Kafka client struct embeds both the producer and consumer implementations:

type Client struct {
    *Producer   // promotes Publish(), Close()
    *Consumer   // promotes Subscribe(), Run()
}

// Client automatically satisfies ProducerConsumer interface
// without explicitly implementing any methods!
var _ ProducerConsumer = (*Client)(nil)  // compile-time check

Model Inheritance via Embedding

Domain models share base fields through embedding, avoiding field duplication:

// Base model with shared fields
type Tenant struct {
    ID        uuid.UUID  `json:"id"`
    Name      string     `json:"name"`
    Domain    string     `json:"domain"`
    // ... common fields
}

// Create variant adds upload-specific fields
type TenantCreateUpload struct {
    Tenant                          // embedded: all Tenant fields are promoted
    Logo    *multipart.FileHeader  `form:"logo"`
}

// Relational variant adds join data
type TenantRelasi struct {
    Tenant                          // embedded base
    Users   []User               `json:"users"`
    Config  *TenantConfig        `json:"config"`
}

Multi-Level Embedding

Embedding can be chained to build up complex models from simple building blocks:

type Permission struct {
    ID     uuid.UUID `json:"id"`
    Action string    `json:"action"`
}

type RoleMenu struct {
    Permission                  // level 1: inherits Action
    Menu   string `json:"menu"`
}

type RoleMenuRelasi struct {
    RoleMenu                    // level 2: inherits Permission + Menu
    Role   string `json:"role"`
}

// RoleMenuRelasi.Action works — promoted through two levels

Shared Data Embedding

When two models share the same data fields but differ in metadata, factor out the shared part:

type BroadcastCampaignData struct {
    Name        string `json:"name"`
    TemplateID  string `json:"template_id"`
    ScheduledAt *time.Time `json:"scheduled_at"`
    // ... shared fields
}

// Read model: has ID, timestamps
type BroadcastCampaign struct {
    ID        uuid.UUID `json:"id"`
    BroadcastCampaignData
    CreatedAt time.Time `json:"created_at"`
}

// Write model: no ID (generated server-side)
type BroadcastCampaignCreate struct {
    BroadcastCampaignData
    CreatedBy uuid.UUID `json:"created_by"`
}
Key insight: Method promotion means that if BroadcastCampaignData has a Validate() method, both BroadcastCampaign and BroadcastCampaignCreate automatically get it. If either type defines its own Validate(), it shadows the embedded version — Go's equivalent of "overriding" without inheritance.

4.3 Reflection

reflect

WHAT

Go's reflect package allows inspecting types and values at runtime. The codebase uses it sparingly for two specific purposes: dynamic PATCH updates and validator tag registration.

WHY

For PATCH-style HTTP updates, the server needs to distinguish between "the client sent name: """ (intentionally clearing the field) and "the client did not send name at all" (leave it unchanged). Reflection on pointer fields solves this: a nil pointer means "not sent", a non-nil pointer means "explicitly set".

HOW

NonNilFields — Dynamic PATCH Field Detection

pkg/shared/http/request.go
func NonNilFields(v interface{}) []string {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()  // dereference pointer to struct
    }
    typ := val.Type()

    fields := make([]string, 0)
    for i := 0; i < val.NumField(); i++ {
        fv := val.Field(i)    // field value
        ft := typ.Field(i)    // field type info (name, tags)

        // Skip fields the client didn't send
        if fv.Kind() == reflect.Ptr {
            if fv.IsNil() { continue }  // nil pointer = not sent
        } else {
            if fv.IsZero() { continue }  // zero value = not sent
        }

        // Extract the JSON field name from struct tag
        jsonTag := ft.Tag.Get("json")
        column := strings.Split(jsonTag, ",")[0]
        if column != "" && column != "-" {
            fields = append(fields, column)
        }
    }
    return fields
}
Usage: A PATCH handler calls NonNilFields(updateRequest) to get ["name", "email"] — only the fields the client actually provided. This list is then passed to GORM's Select(fields...).Updates() to update only those columns, leaving untouched fields unchanged in the database.

Validator Tag Name Registration

pkg/shared/validator/validator.go
// Register custom tag name function so validation errors
// use JSON field names instead of Go struct field names
Validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
    name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    if name == "-" {
        return ""
    }
    return name
})
Why this matters: Without this, a validation error for FirstName string `json:"first_name" validate:"required"` would report "FirstName is required". With the tag name function, it reports "first_name is required" — matching the API contract the client sees.

4.4 Defer Patterns

defer

WHAT

defer schedules a function call to run when the surrounding function returns. Deferred calls execute in LIFO order (last deferred = first executed) and run even if the function panics. The codebase uses 8 distinct defer patterns.

WHY

Defer guarantees cleanup happens regardless of how the function exits — normal return, early error return, or panic. This prevents resource leaks (open connections, unreleased locks, unfinished spans) that are otherwise easy to miss in complex control flow.

HOW

1. Mutex Unlock

Prevents deadlocks even if the function panics between lock and unlock:

s.mu.RLock()
defer s.mu.RUnlock()

// ... read shared state ...
// RUnlock runs even if panic occurs here

2. HTTP Response Body Close

Every http.Get/http.Do call requires closing the body to release the TCP connection:

resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()  // prevents connection leak

// ... read resp.Body ...

3. OpenTelemetry Span End

Records the span duration and marks it complete, regardless of success or failure:

ctx, span := tracer.Start(ctx, "service.CreateCampaign")
defer span.End()  // captures total execution time

// ... business logic ...
// span.End() runs last, recording the full duration

4. Nested Defer for Shutdown

When cleanup itself requires a context with a timeout:

defer func() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()  // inner defer: cancel the timeout context
    if err := tracerProvider.Shutdown(ctx); err != nil {
        slog.Error("tracer shutdown failed", "error", err)
    }
}()
Why the nested defer? The shutdown context (5s timeout) is only needed during shutdown. Creating it earlier would waste resources. The outer defer triggers on function exit; the inner defer cancel() cleans up the timeout context.

5. Panic Recovery in Workers

Prevents a single message from crashing the entire consumer process:

defer func() {
    if r := recover(); r != nil {
        log.Printf("panic recovered in worker: %v", r)
        // Log stack trace, increment error metric
        // Worker continues processing next message
    }
}()

6. Combined Cleanup (Semaphore + WaitGroup)

A single defer releases multiple resources atomically:

sem <- struct{}{}  // acquire semaphore slot
wg.Add(1)
go func() {
    defer func() {
        <-sem       // release semaphore slot
        wg.Done()   // signal WaitGroup
    }()
    // ... process message ...
}()
Why group them? Both the semaphore release and wg.Done() must happen on every exit path (success, error, panic). A single defer func() guarantees both run together, preventing slot leaks or shutdown hangs.

7. Ticker Stop

Prevents the ticker's internal goroutine from leaking when the cleanup loop exits:

ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()  // releases ticker resources

for range ticker.C {
    s.cleanup()
}

8. GORM Transaction Rollback

Auto-rollback if the function returns without committing:

tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

// ... multiple DB operations ...

if err != nil {
    tx.Rollback()
    return err
}
return tx.Commit().Error
Pattern Resource Protected Risk Without Defer
Mutex unlock Shared memory Deadlock on panic
Body close TCP connection Connection pool exhaustion
Span end Trace data Missing/infinite spans
Shutdown External services Data loss on exit
Panic recovery Process stability Worker crash
Semaphore + WG Concurrency control Goroutine leak / shutdown hang
Ticker stop Internal goroutine Goroutine leak
Transaction rollback Database consistency Uncommitted transaction locks

4.5 Closures & Middleware Factories

Closure Middleware

WHAT

A closure is a function that captures variables from its enclosing scope. In the codebase, closures are used extensively as middleware factories — functions that accept configuration and return a func(http.Handler) http.Handler middleware.

WHY

The middleware signature func(http.Handler) http.Handler is Chi's (and Go's) standard middleware interface. It enables chaining: r.Use(Auth, CORS, RateLimit). But most middleware needs configuration (JWT secret, allowed roles, base domain). Closures capture this configuration at setup time, creating a configured middleware that matches the required signature.

HOW

JWT Middleware Factory

pkg/auth/middleware.go
func JWTMiddleware(cfg JWTConfig) func(http.Handler) http.Handler {
    // ① Factory: returns a middleware closure
    return func(next http.Handler) http.Handler {
        // ② Middleware: returns an HTTP handler closure
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // ③ cfg is captured from the outer scope (closure!)
            claims, err := ValidateJWT(r, cfg)
            if err != nil {
                http.Error(w, "unauthorized", 401)
                return
            }
            ctx := context.WithValue(r.Context(), claimsKey, claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// Usage at router setup:
r.Use(JWTMiddleware(jwtConfig))  // cfg captured once, used per-request
Three levels of nesting: (1) Factory call (runs once at startup) → (2) Middleware wrapping (runs once per route registration) → (3) Handler execution (runs per request). The cfg variable lives in level 1 and is captured by level 3.

Other Middleware Factories in the Codebase

Factory Function Captured Variable Purpose
TenantIdentifierMiddleware(baseDomain) baseDomain string Extracts tenant from subdomain by stripping the base domain
Permission(menu, action) menu, action string Checks if JWT claims include the required menu+action permission
RequireRole(roles...) roles []string Restricts access to specific roles (superadmin, admin, etc.)
IdempotentHandler(store, ttl, handler) store, ttl, handler Wraps event handler with idempotency check to prevent duplicate processing
RetryableHandler(cfg, producer, handler) cfg, producer, handler Wraps event handler with retry logic and dead-letter queue publishing
Composition in action: These closures compose beautifully: RetryableHandler(cfg, producer, IdempotentHandler(store, ttl, actualHandler)) — each layer adds one concern (retry, idempotency) without modifying the inner handler.

4.6 Type Assertions & Type Switches

Type Switch Type Assertion

WHAT

Type assertions extract the concrete type from an interface value. A type switch branches on the concrete type. The codebase uses both for value conversion, context extraction, JWT claims parsing, validation error handling, and response wrapping.

WHY

Go's interfaces are satisfied implicitly, which means functions often receive interface{} / any values. Type assertions and switches restore type safety at the boundaries where concrete types matter (JSON parsing, context values, error classification).

HOW

Type Switch for Value Conversion

pkg/shared/helpers/convert.go
func ToString(v interface{}) string {
    switch val := v.(type) {
    case string:
        return val
    case int:
        return strconv.Itoa(val)
    case float64:
        return strconv.FormatFloat(val, 'f', -1, 64)
    case bool:
        return strconv.FormatBool(val)
    case nil:
        return ""
    default:
        return fmt.Sprintf("%v", val)
    }
}
Why not fmt.Sprintf for everything? The type switch handles each type's formatting idiomatically (no trailing zeros for floats, proper nil handling). The default case is the safety net for unexpected types.

Comma-Ok Assertion for Context Values

pkg/auth/context.go
func GetTenantContext(ctx context.Context) (TenantContext, bool) {
    tc, ok := ctx.Value(tenantContextKey).(TenantContext)
    return tc, ok
    // ok == false means the middleware hasn't run yet
    // (programming error, not user error)
}
Always use comma-ok. A bare assertion ctx.Value(key).(TenantContext) panics if the value is nil or a different type. The comma-ok form returns ok=false instead, allowing graceful error handling.

JWT Claims Extraction

// JWT claims are map[string]interface{} — type assertions needed
sub, ok := claims["sub"].(string)
if !ok {
    return nil, errors.New("invalid subject claim")
}

exp, ok := claims["exp"].(float64)  // JSON numbers decode to float64
if !ok {
    return nil, errors.New("invalid expiry claim")
}

Validation Error Type Assertion

if err := Validate.Struct(input); err != nil {
    // Check if it's a validation error (vs. a reflect panic, etc.)
    ve, ok := err.(validator.ValidationErrors)
    if ok {
        // Convert to API-friendly error messages
        for _, fe := range ve {
            errors = append(errors, formatFieldError(fe))
        }
    }
}

Huma Response Wrapping (Avoid Double-Wrap)

// Type switch to avoid wrapping a Response inside another Response
switch v := data.(type) {
case Response[any]:
    // Already wrapped, use as-is
    return v
default:
    // Wrap in standard Response envelope
    return Response[any]{Success: true, Data: v}
}

4.7 Pointer vs Value Receivers

Receivers

WHAT

Go methods can use either a pointer receiver (func (s *Service) Method()) or a value receiver (func (s Service) Method()). The choice affects whether the method can mutate the receiver and which types satisfy an interface.

WHY

The rule: methods on a pointer receiver are only in the method set of *T, not T. Methods on a value receiver are in the method set of both T and *T. This distinction determines which types satisfy interfaces, and the codebase uses each deliberately.

HOW

Pointer Receiver (Dominant)

// All services, repositories, handlers:
func (s *service) Create(ctx context.Context, req CreateRequest) error {
    // s.db, s.cache, s.mu are mutable
    return s.repo.Create(ctx, req)
}

Why: Avoids copying large structs (db connections, caches, mutexes). A sync.Mutex must never be copied — pointer receiver is mandatory when the struct contains one.

Value Receiver (Rare)

// helpers/permanent_error.go
type PermanentError struct {
    Err error
}

func (e PermanentError) Error() string {
    return e.Err.Error()
}

Why: Value receiver means both PermanentError and *PermanentError satisfy the error interface. Callers can use either form: PermanentError{Err: err} or &PermanentError{Err: err}.

Contrast: Pointer Receiver for Error

pkg/eventbus/errors.go
// eventbus.PermanentError uses pointer receiver
func (e *PermanentError) Error() string {
    return e.Err.Error()
}

// Type assertion MUST use pointer:
var permErr *PermanentError
if errors.As(err, &permErr) {
    // non-retryable error
}
Subtle difference: With a pointer receiver, only *PermanentError satisfies error. The errors.As target variable must be declared as *PermanentError, not PermanentError. This is a common Go gotcha.

GORM TableName — Value Receiver for Constants

func (RoleMenuRelasi) TableName() string {
    return "role_menus"
}
// Value receiver because:
// 1. Returns a constant — no mutation needed
// 2. No receiver variable name needed (not accessed)
// 3. Works with both value and pointer types

4.8 Function Types as Values

Function Type Strategy Pattern

WHAT

Go functions are first-class values. The codebase defines named function types and passes them as arguments, enabling a lightweight Strategy pattern without the ceremony of defining an interface + struct + method.

WHY

When behavior varies by a single function (event handler, retry strategy, validation rule), a function type is simpler than a full interface. The caller passes a function literal directly, without needing to define a named struct that implements a single-method interface.

HOW

pkg/eventbus/types.go
// Named function type — acts as a contract
type Handler func(ctx context.Context, event *Event) error

This type is used as a first-class value throughout the event system:

// 1. Passed to Subscribe() — tells the consumer what to do with events
consumer.Subscribe("broadcast.messages", "worker-group", messageHandler)

// 2. Wrapped by IdempotentHandler() — adds dedup logic around any handler
idempotent := IdempotentHandler(store, 24*time.Hour, messageHandler)

// 3. Wrapped by RetryableHandler() — adds retry logic around any handler
retryable := RetryableHandler(retryCfg, producer, idempotent)

// 4. Composed: retry wraps idempotency wraps actual handler
consumer.Subscribe("broadcast.messages", "worker-group", retryable)
Strategy pattern, Go-style. In Java, you'd define a MessageHandler interface, implement it in 3 classes, and use decorator pattern. In Go, a function type + closures achieves the same composition in fewer lines and with less indirection.

4.9 Init Functions

init()

WHAT

Go's init() functions run automatically when a package is imported, before main(). They cannot be called explicitly and have no parameters or return values.

WHY

The 1Engage codebase has only ONE init() function in the entire project. This is deliberate and notable — the team avoids init() magic, preferring explicit initialization in constructors and main(). The single exception is for global validator configuration that must happen exactly once before any validation call.

HOW

pkg/shared/validator/validator.go
var (
    Validate    = validator.New()
    FormDecoder = form.NewDecoder()
)

func init() {
    // Configure the form decoder to use "form" struct tags
    FormDecoder.SetTagName("form")

    // Tell the validator to use JSON tag names in error messages
    Validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
        name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
        if name == "-" {
            return ""
        }
        return name
    })
}
Good practice: Using init() here is justified because Validate and FormDecoder are package-level singletons that every handler imports. The configuration must happen exactly once, before any usage. Putting it in init() guarantees this without requiring callers to remember to call a setup function.
Why only one? init() drawbacks include: no explicit ordering across packages, hidden side effects, harder testing (can't skip or re-run), and no error return. The team rightly uses explicit New*() constructors and cfg.Setup() patterns everywhere else.

4.10 Build-Time Variable Injection

-ldflags Build Info

WHAT

Go's linker flag -ldflags "-X" overrides string variables at compile time. This injects version info, git commit hash, and build timestamp into the binary without modifying source code.

WHY

Every deployed binary needs to answer: "What version am I? What commit was I built from? When was I compiled?" This information is critical for debugging production incidents, correlating logs with code, and verifying deployments. Build-time injection means the values are baked into the binary — no config files or environment variables needed at runtime.

HOW

pkg/version/version.go
var (
    Version   = "dev"       // overridden at build time
    GitCommit = "unknown"   // overridden at build time
    BuildTime = "unknown"   // overridden at build time
)

Build Command

go build \
    -ldflags "-X pkg/version.Version=1.2.3 \
              -X pkg/version.GitCommit=abc123def \
              -X pkg/version.BuildTime=2025-01-15T10:30:00Z" \
    -o bin/broadcast-service \
    ./apps/broadcast-service/cmd/server

How It Works

Stage Value of Version
Source code (default) "dev"
After go build -ldflags "1.2.3"
At runtime "1.2.3" (baked in, no env lookup)
Default values are the safety net. During local development, go run without -ldflags uses the defaults ("dev", "unknown"). The CI/CD pipeline passes real values. If the values show "unknown" in production, it means the build pipeline is misconfigured — an immediate, visible signal.
Constraints: -X can only set string variables. It cannot set int, bool, or other types. The variable must be a package-level var (not const). The import path must be fully qualified from the module root.

1Engage Multitenant Backend — Codebase Guide — Chapter 04: Go Language Features