Prev Next

Golang / GoLang Production Patterns and Web Standards Interview Questions

1. Why does Go treat errors as values instead of using exceptions, and what are the advantages? 2. How do you wrap errors in Go 1.13+ and use errors.Is and errors.As for inspection? 3. What are the best practices for defining custom error types in Go? 4. What is context.Context in Go, what does it carry, and how do you create one? 5. How does context propagate through an HTTP request lifecycle in Go? 6. What are the best practices and anti-patterns for using context.WithValue? 7. How do you build a production-ready HTTP server using Go's standard net/http package? 8. What is the standard Go HTTP middleware signature and how do you chain multiple middleware? 9. How has http.ServeMux evolved in Go 1.22 and what routing patterns does it support? 10. How do you encode and decode JSON in Go and what are the common pitfalls? 11. What are the rules for writing HTTP responses correctly in Go handlers? 12. How do you implement dependency injection in Go without a framework? 13. How do you implement structured logging in Go using the slog package? 14. What are the idiomatic Go patterns for managing application configuration? 15. How do you interact with a SQL database in Go using the standard database/sql package? 16. How do you implement graceful shutdown of an HTTP server in Go? 17. How do you test HTTP handlers in Go without starting a real server? 18. What are table-driven tests in Go and why are they the preferred testing pattern? 19. How do you write testable Go code using interfaces and mocks without a framework? 20. How do you implement rate limiting in a Go HTTP server? 21. How do you implement CORS correctly in a Go HTTP server? 22. How and when should you use panic and recover in production Go code? 23. How do you manage environment-specific settings and feature flags in Go? 24. How do you implement health check and readiness endpoints for a Go service? 25. How do you add observability (metrics and distributed tracing) to a Go service? 26. How do build tags work in Go and when do you use them? 27. What are the best practices for using Go's HTTP client in production? 28. What patterns make HTTP error handling consistent and DRY in Go? 29. What are the common API versioning strategies in Go HTTP services? 30. How do Go programs handle OS signals and interact with the operating system? 31. How do you profile a Go service in production using pprof? 32. How does Go's module system work and what are the key commands? 33. How do you validate HTTP request inputs in Go without a framework? 34. How do you implement streaming HTTP responses in Go? 35. How do you write and interpret Go benchmarks? 36. How do you embed static files into a Go binary using go:embed? 37. How do you implement timeouts for non-HTTP operations like database queries and external calls? 38. What are the conventions for returning structured error responses from a Go REST API? 39. How do you implement pagination for list endpoints in a Go REST API? 40. How do you implement JWT authentication middleware in Go? 41. How do you implement HTTP response caching in a Go service? 42. How does gRPC work in Go and when would you choose it over REST/JSON? 43. What linters and static analysis tools are essential for production Go code quality? 44. What is the recommended project structure for a production Go service? 45. How do you implement load shedding and request queue limits in a Go HTTP server? 46. How do you correctly propagate errors from concurrent goroutines in a Go service? 47. How do you document a Go REST API and maintain an OpenAPI specification? 48. What is the production readiness checklist for a Go HTTP service?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. Why does Go treat errors as values instead of using exceptions, and what are the advantages?

Go deliberately chose to make errors ordinary values rather than using a try-catch exception mechanism. The error interface has exactly one method:

type error interface { Error() string }

Any function that can fail returns an error as its last return value. The caller must explicitly handle or propagate it. This design philosophy comes from Rob Pike's observation that exception-based code tends to produce programs where error handling is an afterthought — invisible control flow that developers skip over.

Errors-as-Values vs Exceptions
AspectGo errors-as-valuesException-based languages
VisibilityError paths are explicit in every function signatureError paths are hidden; callers may not know a function can fail
HandlingCompiler forces you to receive the error; ignoring requires _Uncaught exceptions terminate the program unexpectedly
Control flowLinear — no hidden jumpsNon-local jumps complicate reasoning
CompositionError values can carry rich contextException hierarchies can become complex
PerformanceNo stack unwinding — cheapStack unwinding has overhead
// Every failing operation explicitly returns an error
func readConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("readConfig: %w", err)
    }
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("readConfig unmarshal: %w", err)
    }
    return &cfg, nil
}

// Caller is forced to decide what to do with the error
cfg, err := readConfig("/etc/app/config.json")
if err != nil {
    log.Fatalf("startup failed: %v", err)
}

Go does have panic and recover, but they are reserved for truly unrecoverable situations (nil pointer dereferences, index out of range) or as an internal implementation detail in libraries that convert panics to errors at the public boundary.

What is the primary design reason Go uses errors-as-values instead of exceptions?
What is the single method of Go's built-in error interface?
2. How do you wrap errors in Go 1.13+ and use errors.Is and errors.As for inspection?

Go 1.13 introduced the %w verb in fmt.Errorf and the errors.Is / errors.As functions to create and inspect error chains. Wrapping preserves the original error while adding context — callers can still check for specific error types or sentinel values anywhere in the chain.

import "errors"

// Sentinel errors — comparable with errors.Is
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
)

// Wrapping with %w — preserves the error chain
func fetchUser(id int) (*User, error) {
    u, err := db.QueryUser(id)
    if err != nil {
        return nil, fmt.Errorf("fetchUser(id=%d): %w", id, err)
    }
    return u, nil
}

// errors.Is — checks the entire chain for a target value
err := fetchUser(42)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "user not found", http.StatusNotFound)
    return
}

// Custom structured error type
type ValidationError struct {
    Field   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("validateAge: %w",
            &ValidationError{Field: "age", Message: "must be non-negative"})
    }
    return nil
}

// errors.As — extracts a specific error type from the chain
var ve *ValidationError
if errors.As(err, &ve) {
    http.Error(w,
        fmt.Sprintf("invalid field %s: %s", ve.Field, ve.Message),
        http.StatusBadRequest)
}

// errors.Unwrap — one level of unwrapping
inner := errors.Unwrap(err)   // returns the wrapped error

// Go 1.20+: errors.Join — wrap multiple errors in one
combined := errors.Join(err1, err2)
errors.Is(combined, err1) // true

Key difference: %v formats the error as a plain string — the wrapped error is lost for inspection purposes. %w both formats it AND preserves the wrapped error so errors.Is and errors.As can traverse the chain.

What does 'fmt.Errorf("context: %w", err)' do that 'fmt.Errorf("context: %v", err)' does not?
What does errors.As(err, &target) do?
3. What are the best practices for defining custom error types in Go?

Custom error types are used when callers need to inspect error details beyond a simple message. The key decision is whether to use a pointer receiver (most common for structs) or a value receiver, and whether to implement the optional Unwrap() method to participate in the error chain.

// Pattern 1: simple sentinel error (no extra data)
var ErrRateLimited = errors.New("rate limited")

// Pattern 2: structured error with context
type HTTPError struct {
    Code    int
    Message string
    Cause   error // optional wrapped cause
}

func (e *HTTPError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("HTTP %d: %s: %v", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}

// Implement Unwrap() to participate in errors.Is / errors.As chain
func (e *HTTPError) Unwrap() error { return e.Cause }

// Constructor — always return concrete type as error interface
func newHTTPError(code int, msg string, cause error) error {
    return &HTTPError{Code: code, Message: msg, Cause: cause}
}

// Usage
func callAPI(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return newHTTPError(0, "request failed", err)
    }
    if resp.StatusCode == http.StatusTooManyRequests {
        return newHTTPError(429, "rate limited", ErrRateLimited)
    }
    return nil
}

// Caller
err := callAPI("https://api.example.com/data")
var httpErr *HTTPError
if errors.As(err, &httpErr) {
    log.Printf("HTTP error %d: %s", httpErr.Code, httpErr.Message)
}
if errors.Is(err, ErrRateLimited) {
    time.Sleep(time.Second) // back off and retry
}

// ANTI-PATTERN: typed nil trap
// func getUser() error {
//     var e *HTTPError
//     return e    // NOT nil interface! {type=*HTTPError, data=nil}
// }
// Always: return nil (bare), not a typed nil pointer
Why must a custom error type implement Unwrap() to work correctly with errors.Is?
What is the 'typed nil trap' in Go error handling?
4. What is context.Context in Go, what does it carry, and how do you create one?

context.Context is Go's standard mechanism for propagating three things across API boundaries and goroutine boundaries: cancellation signals, deadlines/timeouts, and request-scoped values. Every blocking or long-running function should accept a Context as its first parameter.

// The full context.Context interface:
type Context interface {
    Deadline() (deadline time.Time, ok bool) // zero time if no deadline
    Done()     <-chan struct{}                // closed on cancel/timeout
    Err()      error                         // nil, Canceled, or DeadlineExceeded
    Value(key any) any                       // request-scoped value lookup
}

// Root contexts (start of a context tree)
ctx := context.Background() // never cancelled, no deadline — use at program start
ctx  = context.TODO()       // placeholder when context not yet known

// Derived contexts — each returns a cancel function
ctx1, cancel1 := context.WithCancel(context.Background())
defer cancel1()  // ALWAYS defer — prevents goroutine leak in the monitor goroutine

ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()

ctx3, cancel3 := context.WithDeadline(context.Background(),
    time.Now().Add(30*time.Second))
defer cancel3()

// Attaching a value (use typed key to avoid collisions)
type ctxKey string
const traceIDKey ctxKey = "traceID"
ctx4 := context.WithValue(ctx, traceIDKey, "abc-123")
traceID := ctx4.Value(traceIDKey).(string)

// Propagate context to downstream functions
func processRequest(ctx context.Context, req Request) error {
    if err := validateInput(ctx, req); err != nil {
        return fmt.Errorf("validation: %w", err)
    }
    return persistToDB(ctx, req) // ctx carries cancellation + deadline
}

Context tree: cancelling a parent cancels all descendants. A child context with a shorter deadline does not extend the parent's deadline — the effective deadline is always min(parent, child).

What does ctx.Done() return?
Why must you always defer the cancel function returned by context.WithCancel?
5. How does context propagate through an HTTP request lifecycle in Go?

Since Go 1.7, every *http.Request carries a context accessible via r.Context(). This context is cancelled when the client disconnects or the server shuts down. Middleware and handlers should pass this context to all downstream calls — database queries, outbound HTTP calls, gRPC — so they all cancel when the request does.

// The request context is cancelled when the client disconnects
func userHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // inherits client-disconnect cancellation

    // Add a per-request deadline
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    // Pass ctx to ALL downstream blocking calls
    userID := chi.URLParam(r, "id")
    user, err := userService.FindByID(ctx, userID)  // DB call respects deadline
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "request timed out", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

// Making an outbound HTTP call with the request context
func fetchExternalData(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// Middleware: attach request-scoped values to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := r.Header.Get("X-Request-ID")
        if id == "" {
            id = uuid.New().String()
        }
        ctx := context.WithValue(r.Context(), requestIDKey, id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
How do you pass a request context to an outbound HTTP call in Go?
When is the context of an HTTP request automatically cancelled in Go?
6. What are the best practices and anti-patterns for using context.WithValue?

context.WithValue should be used sparingly — only for request-scoped data that crosses API boundaries and would be impractical to pass as explicit parameters. It is not a general-purpose parameter-passing mechanism.

// ANTI-PATTERN: using a plain string as a context key
ctx = context.WithValue(ctx, "userID", 42)
// Any package can read/overwrite this key — key collisions guaranteed

// CORRECT: define an unexported typed key per package
// This prevents any other package from accidentally colliding
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// Better: use a struct type as the key (common in stdlib)
type requestIDKeyType struct{}

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKeyType{}, id)
}

func RequestIDFromContext(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(requestIDKeyType{}).(string)
    return id, ok
}

// GOOD USES of context values:
// - Request/trace IDs for logging and distributed tracing
// - Authentication tokens / user identity
// - Feature flags tied to the request

// BAD USES of context values:
// - Optional function parameters
// - Database connections (pass as explicit dependency)
// - Configuration that should be in a struct
// - Anything that the function should document as a dependency

// The Go proverb: 'Use context values only for request-scoped data
// that transits processes and API boundaries, not for passing
// optional parameters to functions.'

Context value lookups are O(n) in the number of values stored — each WithValue wraps the context in a new layer. Storing many values is inefficient. If you have many related values, wrap them in a single struct: context.WithValue(ctx, reqKey{}, &RequestData{UserID: 1, TraceID: "abc"}).

Why should context keys be unexported typed constants rather than plain strings?
What is the recommended data to store in context values?
7. How do you build a production-ready HTTP server using Go's standard net/http package?

Go's net/http package provides a production-capable HTTP server out of the box. Unlike Node.js or Python frameworks, you rarely need a third-party framework for basic HTTP serving — the standard library handles routing, TLS, graceful shutdown, and concurrent request handling.

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()

    // Register handlers
    mux.HandleFunc("GET /health", healthHandler)  // Go 1.22+ method+path
    mux.HandleFunc("GET /users/{id}", getUserHandler)
    mux.HandleFunc("POST /users", createUserHandler)

    // Apply middleware chain
    handler := loggingMiddleware(requestIDMiddleware(mux))

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  5 * time.Second,   // time to read full request
        WriteTimeout: 10 * time.Second,  // time to write full response
        IdleTimeout:  120 * time.Second, // keep-alive timeout
        MaxHeaderBytes: 1 << 20,         // 1 MB header limit
    }

    // Start server in a goroutine
    go func() {
        log.Printf("server listening on %s", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server error: %v", err)
        }
    }()

    // Graceful shutdown on OS signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("forced shutdown: %v", err)
    }
    log.Println("server stopped")
}

Always set timeouts on production HTTP servers. Without them, slow clients can exhaust goroutines: ReadTimeout prevents slow request bodies, WriteTimeout prevents slow response consumption, and IdleTimeout prevents idle keep-alive connections from accumulating.

What is the purpose of the ReadTimeout in http.Server?
What does http.Server.Shutdown(ctx) do?

8. What is the standard Go HTTP middleware signature and how do you chain multiple middleware?

Go middleware follows a consistent adapter pattern: a function that accepts an http.Handler and returns a new http.Handler that wraps it. The signature func(http.Handler) http.Handler is the community standard — used by virtually every Go HTTP library.

// The standard middleware type
type Middleware func(http.Handler) http.Handler

// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // Wrap ResponseWriter to capture status code
        lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(lrw, r)  // call the next handler in the chain
        log.Printf("%s %s %d %v",
            r.Method, r.URL.Path, lrw.statusCode, time.Since(start))
    })
}

type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}

// Auth middleware
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return // short-circuit — next.ServeHTTP never called
        }
        next.ServeHTTP(w, r)
    })
}

// Recovery middleware — catch panics in handlers
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic recovered: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Chaining middleware — applied right-to-left (outermost runs first)
handler := recoveryMiddleware(loggingMiddleware(authMiddleware(mux)))
// Request order: recovery → logging → auth → mux → handler

// Cleaner chain helper
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}
handler = Chain(mux, recoveryMiddleware, loggingMiddleware, authMiddleware)
What is the standard signature for a Go HTTP middleware function?
In the middleware chain 'recoveryMiddleware(loggingMiddleware(authMiddleware(mux)))', which middleware executes first?
9. How has http.ServeMux evolved in Go 1.22 and what routing patterns does it support?

Go 1.22 significantly upgraded http.ServeMux with method-based routing, path parameters, and wildcard matching — reducing the need for third-party routers in many applications.

mux := http.NewServeMux()

// Go 1.22+ enhanced routing patterns:

// Method + exact path
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status":"ok"}`))
})

// Method + path with named parameter {id}
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")  // extract path variable
    fmt.Fprintf(w, "user %s", id)
})

// Method + path with wildcard {path...} (matches remainder)
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    http.ServeFile(w, r, filepath.Join("/static", path))
})

// POST with body
mux.HandleFunc("POST /users", createUserHandler)

// DELETE
mux.HandleFunc("DELETE /users/{id}", deleteUserHandler)

// Host-based routing
mux.HandleFunc("api.example.com/GET /v1/", apiV1Handler)

// Trailing slash subtree pattern (matches /api/ and all sub-paths)
mux.HandleFunc("/api/", apiHandler)

// Pre-1.22 pattern: no method prefix, manual method check
// mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
//     switch r.Method {
//     case http.MethodGet:  listUsers(w, r)
//     case http.MethodPost: createUser(w, r)
//     default:              http.Error(w, "method not allowed", 405)
//     }
// })

Go 1.22 routing precedence: more specific patterns win over less specific ones. A pattern with a method is more specific than one without. An exact path is more specific than a wildcard. If two patterns conflict (same specificity, overlapping matches), the registration panics at startup — catching routing mistakes at boot time rather than silently returning wrong responses.

How do you extract a path parameter in Go 1.22's http.ServeMux?
In Go 1.22 ServeMux, what does registering 'GET /users/{id}' automatically do for non-GET methods to that path?
10. How do you encode and decode JSON in Go and what are the common pitfalls?

Go's encoding/json package provides json.Marshal/json.Unmarshal for byte slices and json.NewEncoder/json.NewDecoder for streams. The stream-based API is preferred for HTTP handlers since it avoids loading the full body into memory.

// Struct tags control JSON field names and omission
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Password  string    `json:"-"`           // always omit
    CreatedAt time.Time `json:"created_at"`
    Bio       *string   `json:"bio,omitempty"` // omit if nil
    Score     int       `json:"score,omitempty"` // omit if zero
}

// Decoding HTTP request body (preferred: streaming)
func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields() // reject unexpected JSON keys
    if err := dec.Decode(&user); err != nil {
        http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
        return
    }

    // ... process user ...

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

// Common pitfalls:

// 1. Unexported fields are silently ignored
type Hidden struct { public string; private string } // private won't encode

// 2. json.Number for large integers (avoid float64 precision loss)
var data map[string]any
dec := json.NewDecoder(r.Body)
dec.UseNumber() // numbers decoded as json.Number, not float64
dec.Decode(&data)
n, _ := data["id"].(json.Number).Int64()

// 3. Circular references cause Marshal to panic — avoid

// 4. Set Content-Type BEFORE WriteHeader
w.Header().Set("Content-Type", "application/json") // must come first
w.WriteHeader(http.StatusOK)                         // locks headers
Why is json.NewDecoder(r.Body).Decode(&v) preferred over json.Unmarshal for HTTP handlers?
What does the json struct tag 'json:"-"' do?
11. What are the rules for writing HTTP responses correctly in Go handlers?

The http.ResponseWriter interface has ordering rules that are easy to violate, leading to subtle bugs where headers are silently lost or the response is malformed.

// http.ResponseWriter interface:
// type ResponseWriter interface {
//     Header() http.Header    // returns the header map (modify before WriteHeader)
//     WriteHeader(statusCode int) // sets status; can only be called once
//     Write([]byte) (int, error)  // writes body; implicitly calls WriteHeader(200)
// }

// CORRECT ordering:
func goodHandler(w http.ResponseWriter, r *http.Request) {
    // 1. Set headers first
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", "abc-123")

    // 2. Set status code
    w.WriteHeader(http.StatusCreated) // 201

    // 3. Write body
    json.NewEncoder(w).Encode(map[string]string{"status": "created"})
}

// WRONG: setting headers after WriteHeader — silently ignored
func badHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Header().Set("Content-Type", "application/json") // TOO LATE — ignored!
    w.Write([]byte(`{"ok":true}`))
}

// WRONG: calling Write before setting Content-Type
// Write triggers an implicit WriteHeader(200) — headers lock

// Helper: structured JSON response
func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(v); err != nil {
        log.Printf("writeJSON: %v", err)
    }
}

// IMPORTANT: returning from a handler does NOT automatically
// stop a response from being written. Use explicit return after error:
func handler(w http.ResponseWriter, r *http.Request) {
    if err := process(r); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return // MUST return — otherwise code below also runs
    }
    writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
}
What happens if you call w.Header().Set() after w.WriteHeader() in a Go HTTP handler?
What does calling w.Write() without first calling w.WriteHeader() do?
12. How do you implement dependency injection in Go without a framework?

Go's idiomatic approach to dependency injection is constructor-based: pass dependencies as arguments to constructors that return concrete types. This is simpler, more testable, and more readable than reflection-based DI frameworks.

// Define dependencies as interfaces (for testability)
type UserRepository interface {
    FindByID(ctx context.Context, id int) (*User, error)
    Save(ctx context.Context, user *User) error
}

type EmailSender interface {
    Send(ctx context.Context, to, subject, body string) error
}

// Service accepts interfaces — can be mocked in tests
type UserService struct {
    repo  UserRepository
    email EmailSender
    logger *slog.Logger
}

// Constructor injection — explicit, no magic
func NewUserService(
    repo  UserRepository,
    email EmailSender,
    logger *slog.Logger,
) *UserService {
    return &UserService{repo: repo, email: email, logger: logger}
}

func (s *UserService) Register(ctx context.Context, req RegisterRequest) (*User, error) {
    user := &User{Name: req.Name, Email: req.Email}
    if err := s.repo.Save(ctx, user); err != nil {
        return nil, fmt.Errorf("UserService.Register: %w", err)
    }
    s.email.Send(ctx, user.Email, "Welcome!", "Thanks for joining.")
    return user, nil
}

// Wire everything together in main()
func main() {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    repo  := postgres.NewUserRepository(db)
    email := smtp.NewEmailSender(os.Getenv("SMTP_HOST"))
    svc   := NewUserService(repo, email, logger)

    h := handlers.NewUserHandler(svc)
    mux := http.NewServeMux()
    mux.Handle("/users/", h)
    http.ListenAndServe(":8080", mux)
}

Testing advantage: because dependencies are injected as interfaces, tests pass mock implementations without any framework or code generation. A mock UserRepository is just a struct implementing the interface with test-controlled responses.

What is the primary advantage of accepting interface types as constructor parameters in Go?
Where should all dependencies be wired together in a Go application?
13. How do you implement structured logging in Go using the slog package?

log/slog was added to the standard library in Go 1.21 as the official structured logging solution. It produces machine-readable output (JSON or key-value pairs) and supports levels, attributes, and handler customisation.

import "log/slog"

// Default logger — uses text format to stderr
slog.Info("server starting", "port", 8080)
slog.Warn("connection slow", "latency_ms", 450, "host", "db.internal")
slog.Error("request failed", "error", err, "path", r.URL.Path)

// JSON handler for production (machine-parseable)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo, // minimum level to emit
    AddSource: true,       // include file:line in output
}))
slog.SetDefault(logger)

// Output: {"time":"2024-01-15T...","level":"INFO","msg":"user created","id":42}

// Grouping related attributes
logger.Info("request completed",
    slog.Group("request",
        slog.String("method", r.Method),
        slog.String("path", r.URL.Path),
        slog.Int("status", 200),
        slog.Duration("duration", time.Since(start)),
    ),
)

// Logger with pre-set attributes (child logger)
reqLogger := logger.With(
    slog.String("request_id", requestID),
    slog.String("user_id", userID),
)
reqLogger.Info("fetching user data")
reqLogger.Info("user data fetched", "count", len(users))

// Context-aware logging — attach logger to context
func withLogger(ctx context.Context, logger *slog.Logger) context.Context {
    return context.WithValue(ctx, loggerKey{}, logger)
}
func loggerFromCtx(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { return l }
    return slog.Default()
}
What advantage does slog.NewJSONHandler provide over the standard log package?
What does logger.With(...) return in slog?
14. What are the idiomatic Go patterns for managing application configuration?

Go applications typically load configuration from environment variables (twelve-factor app pattern), config files, or a combination. The idiomatic approach is to load all configuration at startup, validate it, and inject it into components as a struct — not read environment variables throughout the codebase.

// Config struct — single source of truth
type Config struct {
    HTTP struct {
        Port         int           `env:"HTTP_PORT"          default:"8080"`
        ReadTimeout  time.Duration `env:"HTTP_READ_TIMEOUT"  default:"5s"`
        WriteTimeout time.Duration `env:"HTTP_WRITE_TIMEOUT" default:"10s"`
    }
    Database struct {
        URL             string `env:"DATABASE_URL"      required:"true"`
        MaxConnections  int    `env:"DB_MAX_CONNECTIONS" default:"25"`
    }
    Auth struct {
        JWTSecret string        `env:"JWT_SECRET" required:"true"`
        TokenTTL  time.Duration `env:"TOKEN_TTL"  default:"24h"`
    }
}

// Load from environment — validate at startup
func loadConfig() (*Config, error) {
    var cfg Config
    // Use envconfig, viper, or manual os.Getenv
    cfg.HTTP.Port = mustEnvInt("HTTP_PORT", 8080)
    cfg.Database.URL = mustEnv("DATABASE_URL")
    cfg.Auth.JWTSecret = mustEnv("JWT_SECRET")
    if cfg.Auth.JWTSecret == "" {
        return nil, errors.New("JWT_SECRET must not be empty")
    }
    return &cfg, nil
}

func mustEnv(key string) string {
    v := os.Getenv(key)
    if v == "" {
        log.Fatalf("required environment variable %s is not set", key)
    }
    return v
}

func main() {
    cfg, err := loadConfig()
    if err != nil {
        log.Fatalf("invalid configuration: %v", err)
    }
    // Inject cfg into all components
    db := openDB(cfg.Database.URL, cfg.Database.MaxConnections)
    srv := newServer(cfg.HTTP, db)
    srv.Start()
}
Why should configuration be loaded and validated at application startup rather than reading os.Getenv throughout the codebase?
What is the benefit of grouping all configuration into a single struct?
15. How do you interact with a SQL database in Go using the standard database/sql package?

Go's database/sql package provides a driver-agnostic interface for SQL databases. It manages a connection pool automatically. The key patterns are: always close rows, always use parameterised queries to prevent SQL injection, and pass context to every query.

import (
    "database/sql"
    _ "github.com/lib/pq" // side-effect import registers the driver
)

// Open — creates the pool, does NOT establish a connection yet
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)

// Verify connection
if err := db.PingContext(ctx); err != nil {
    log.Fatalf("database unreachable: %v", err)
}

// SELECT — always pass context
func getUserByID(ctx context.Context, db *sql.DB, id int) (*User, error) {
    row := db.QueryRowContext(ctx,
        "SELECT id, name, email FROM users WHERE id = $1", id)
    var u User
    err := row.Scan(&u.ID, &u.Name, &u.Email)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrNotFound
    }
    if err != nil {
        return nil, fmt.Errorf("getUserByID: %w", err)
    }
    return &u, nil
}

// SELECT multiple rows — MUST close rows
func listUsers(ctx context.Context, db *sql.DB) ([]User, error) {
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
    if err != nil {
        return nil, fmt.Errorf("listUsers query: %w", err)
    }
    defer rows.Close()  // ALWAYS close — frees connection back to pool

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name); err != nil {
            return nil, fmt.Errorf("listUsers scan: %w", err)
        }
        users = append(users, u)
    }
    return users, rows.Err() // check iteration error
}

// Transaction
func transferFunds(ctx context.Context, db *sql.DB, from, to, amount int) error {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil { return err }
    defer tx.Rollback() // no-op if Commit succeeds

    if _, err := tx.ExecContext(ctx,
        "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, from); err != nil {
        return fmt.Errorf("debit: %w", err)
    }
    if _, err := tx.ExecContext(ctx,
        "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to); err != nil {
        return fmt.Errorf("credit: %w", err)
    }
    return tx.Commit()
}
Why must you always call rows.Close() after a database/sql Query?
What does 'defer tx.Rollback()' in a transaction function achieve?
16. How do you implement graceful shutdown of an HTTP server in Go?

Graceful shutdown means: stop accepting new connections, wait for in-flight requests to complete, then exit. This prevents data loss and connection resets for clients whose requests are mid-flight when a deployment or restart occurs.

func main() {
    srv := &http.Server{
        Addr:    ":8080",
        Handler: buildRouter(),
    }

    // Start server in background
    serverErr := make(chan error, 1)
    go func() {
        log.Println("listening on :8080")
        if err := srv.ListenAndServe(); err != nil {
            serverErr <- err
        }
    }()

    // Wait for shutdown signal or server error
    shutdown := make(chan os.Signal, 1)
    signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)

    select {
    case err := <-serverErr:
        if !errors.Is(err, http.ErrServerClosed) {
            log.Fatalf("server error: %v", err)
        }
    case sig := <-shutdown:
        log.Printf("received signal: %v — shutting down", sig)
    }

    // Give in-flight requests up to 30s to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        // Deadline exceeded: force close remaining connections
        log.Printf("forced shutdown: %v", err)
        srv.Close()
    }

    // Clean up other resources
    db.Close()
    log.Println("shutdown complete")
}

// In Kubernetes: SIGTERM is sent ~30s before SIGKILL
// The 30s graceful period must be < terminationGracePeriodSeconds
// Also set preStop lifecycle hook to delay pod removal from endpoints
What is the difference between srv.Shutdown(ctx) and srv.Close()?
Why is http.ErrServerClosed checked when receiving an error from ListenAndServe?
17. How do you test HTTP handlers in Go without starting a real server?

Go's net/http/httptest package provides httptest.NewRecorder() (a fake ResponseWriter) and httptest.NewServer() (a real TCP server on a random port) for testing HTTP code without network overhead.

import (
    "net/http/httptest"
    "testing"
)

// Unit test: test handler in isolation with mock dependencies
func TestGetUserHandler(t *testing.T) {
    // Setup mock repository
    mockRepo := &MockUserRepo{
        FindByIDFunc: func(ctx context.Context, id int) (*User, error) {
            return &User{ID: id, Name: "Alice"}, nil
        },
    }
    svc := NewUserService(mockRepo)
    h   := NewUserHandler(svc)

    // Build a request
    req := httptest.NewRequest(http.MethodGet, "/users/1", nil)
    req.SetPathValue("id", "1") // Go 1.22: set path params

    // Capture the response
    rec := httptest.NewRecorder()
    h.GetUser(rec, req)

    // Assert
    res := rec.Result()
    if res.StatusCode != http.StatusOK {
        t.Errorf("expected 200, got %d", res.StatusCode)
    }
    if ct := res.Header.Get("Content-Type"); ct != "application/json" {
        t.Errorf("expected application/json, got %s", ct)
    }
    var user User
    json.NewDecoder(res.Body).Decode(&user)
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}

// Integration test: use httptest.NewServer for real HTTP round-trip
func TestAPIIntegration(t *testing.T) {
    srv := httptest.NewServer(buildRouter())
    defer srv.Close() // frees the port

    resp, err := http.Get(srv.URL + "/health")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        t.Errorf("expected 200, got %d", resp.StatusCode)
    }
}
What does httptest.NewRecorder() provide in Go tests?
When should you use httptest.NewServer instead of httptest.NewRecorder?
18. What are table-driven tests in Go and why are they the preferred testing pattern?

Table-driven tests define multiple test cases as a slice of structs, then iterate over them running each case. This is Go's idiomatic testing pattern — it reduces duplication, makes it easy to add new cases, and provides clear failure messages identifying which case failed.

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     float64
        want     float64
        wantErr  bool
    }{
        {name: "positive division",    a: 10, b: 2, want: 5, wantErr: false},
        {name: "negative result",      a: -6, b: 3, want: -2, wantErr: false},
        {name: "division by zero",     a: 5,  b: 0, want: 0,  wantErr: true},
        {name: "float division",       a: 7,  b: 2, want: 3.5, wantErr: false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := divide(tt.a, tt.b)

            if (err != nil) != tt.wantErr {
                t.Errorf("divide(%v, %v) error = %v, wantErr = %v",
                    tt.a, tt.b, err, tt.wantErr)
                return
            }
            if !tt.wantErr && got != tt.want {
                t.Errorf("divide(%v, %v) = %v, want %v",
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

// Run specific subtests: go test -run TestDivide/division_by_zero
// Parallel subtests:
for _, tt := range tests {
    tt := tt // capture range var (pre-Go 1.22)
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel()  // run subtests in parallel
        // ...
    })
}
What is the primary benefit of using t.Run() for table-driven test cases?
What must you do before Go 1.22 when using a range loop variable inside a t.Run closure?
19. How do you write testable Go code using interfaces and mocks without a framework?

Go's implicit interface satisfaction means any struct with the right methods satisfies an interface — no registration or code generation required. This makes hand-written mocks straightforward, readable, and dependency-free.

// Interface in the consumer package (not the producer)
type EmailService interface {
    Send(ctx context.Context, to, subject, body string) error
}

// Production implementation
type SMTPEmailService struct { host string; port int }
func (s *SMTPEmailService) Send(ctx context.Context, to, subject, body string) error {
    // ... real SMTP logic
    return nil
}

// Hand-written mock for tests
type MockEmailService struct {
    Calls    []string // track what was called
    ReturnErr error   // control what error to return
}
func (m *MockEmailService) Send(ctx context.Context, to, subject, body string) error {
    m.Calls = append(m.Calls, to)
    return m.ReturnErr
}

// Functional mock — more flexible, inline in test
type EmailServiceFunc func(ctx context.Context, to, subject, body string) error
func (f EmailServiceFunc) Send(ctx context.Context, to, subject, body string) error {
    return f(ctx, to, subject, body)
}

// Test using the mock
func TestWelcomeEmail(t *testing.T) {
    mock := &MockEmailService{}
    svc  := NewUserService(nil, mock, slog.Default())

    err := svc.Register(context.Background(), RegisterRequest{
        Name: "Alice", Email: "alice@example.com",
    })
    if err != nil { t.Fatal(err) }

    if len(mock.Calls) != 1 {
        t.Errorf("expected 1 email sent, got %d", len(mock.Calls))
    }
    if mock.Calls[0] != "alice@example.com" {
        t.Errorf("email sent to wrong address: %s", mock.Calls[0])
    }
}

// Test error path
func TestWelcomeEmailFailure(t *testing.T) {
    mock := &MockEmailService{ReturnErr: errors.New("smtp down")}
    svc  := NewUserService(nil, mock, slog.Default())
    // Test that service handles email failure gracefully
}
What makes Go interfaces ideal for testing without a mocking framework?
Where should an interface be defined — in the producer or consumer package?
20. How do you implement rate limiting in a Go HTTP server?

Rate limiting protects services from overload. Go provides the token bucket algorithm via golang.org/x/time/rate. Production implementations typically rate-limit per client IP or API key, not globally.

import "golang.org/x/time/rate"

// Global limiter (simple, not per-client)
var globalLimiter = rate.NewLimiter(rate.Limit(100), 10)
// 100 requests/second with burst of 10

func globalRateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !globalLimiter.Allow() {
            http.Error(w, "too many requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Per-client rate limiter (production pattern)
type IPRateLimiter struct {
    mu       sync.Mutex
    limiters map[string]*rate.Limiter
    r        rate.Limit
    b        int
}

func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
    return &IPRateLimiter{limiters: make(map[string]*rate.Limiter), r: r, b: b}
}

func (l *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
    l.mu.Lock()
    defer l.mu.Unlock()
    if lim, ok := l.limiters[ip]; ok {
        return lim
    }
    lim := rate.NewLimiter(l.r, l.b)
    l.limiters[ip] = lim
    return lim
}

func (l *IPRateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip, _, _ := net.SplitHostPort(r.RemoteAddr)
        if !l.getLimiter(ip).Allow() {
            w.Header().Set("Retry-After", "1")
            http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Blocking wait: hold request until token available (or ctx expires)
if err := limiter.Wait(ctx); err != nil {
    http.Error(w, "request cancelled", http.StatusServiceUnavailable)
}
What algorithm does golang.org/x/time/rate implement?
What is the difference between limiter.Allow() and limiter.Wait(ctx)?
21. How do you implement CORS correctly in a Go HTTP server?

Cross-Origin Resource Sharing (CORS) is required when a browser-based frontend on one domain calls an API on a different domain. Go has no built-in CORS support — you implement it as middleware or use a library like rs/cors.

// Manual CORS middleware (for learning — use a library in production)
func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
    originSet := make(map[string]bool)
    for _, o := range allowedOrigins { originSet[o] = true }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")

            if originSet[origin] {
                w.Header().Set("Access-Control-Allow-Origin", origin)
                w.Header().Set("Access-Control-Allow-Methods",
                    "GET, POST, PUT, DELETE, OPTIONS")
                w.Header().Set("Access-Control-Allow-Headers",
                    "Content-Type, Authorization, X-Request-ID")
                w.Header().Set("Access-Control-Allow-Credentials", "true")
                w.Header().Set("Access-Control-Max-Age", "86400") // 24h preflight cache
            }

            // Handle preflight OPTIONS request
            if r.Method == http.MethodOptions {
                w.WriteHeader(http.StatusNoContent)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// Usage
handler := corsMiddleware([]string{
    "https://app.example.com",
    "https://admin.example.com",
})(mux)

// Production: use github.com/rs/cors
// c := cors.New(cors.Options{
//     AllowedOrigins:   []string{"https://app.example.com"},
//     AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
//     AllowedHeaders:   []string{"Authorization", "Content-Type"},
//     AllowCredentials: true,
// })
// handler = c.Handler(mux)

Security note: never use Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true — browsers reject this combination. Always whitelist specific origins when credentials are involved.

What HTTP method does a browser send for a CORS preflight request?
Why can't you use 'Access-Control-Allow-Origin: *' with 'Access-Control-Allow-Credentials: true'?
22. How and when should you use panic and recover in production Go code?

The Go convention is clear: panics are for unrecoverable programmer errors (nil pointer dereference, index out of bounds, type assertion failure). Libraries should never let panics propagate to callers — they convert panics to errors at the public API boundary.

// Library pattern: convert internal panics to errors at the boundary
func safelyParseTemplate(tmpl string) (result string, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("template parse panicked: %v", r)
        }
    }()
    // This might panic on invalid template syntax in some implementations
    result = parseTemplate(tmpl)
    return result, nil
}

// HTTP recovery middleware — catch handler panics gracefully
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // Log the full stack trace
                buf := make([]byte, 4096)
                n := runtime.Stack(buf, false)
                log.Printf("panic in %s %s: %v\n%s",
                    r.Method, r.URL.Path, err, buf[:n])
                // Return 500 to the client
                http.Error(w, "internal server error",
                    http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// When panic IS appropriate:
// 1. Programmer invariant violation that cannot be recovered
func mustPositive(n int) int {
    if n <= 0 { panic(fmt.Sprintf("expected positive, got %d", n)) }
    return n
}

// 2. Impossible type assertion that should never fail if code is correct
// 3. Test helpers: t.Fatalf is analogous — immediate stop

// When NOT to use panic:
// - File not found, network failure, validation errors → return error
// - Any expected failure condition → return error
In which case is using panic appropriate in production Go code?
What pattern do Go libraries use to prevent internal panics from reaching callers?
23. How do you manage environment-specific settings and feature flags in Go?

Production Go applications distinguish between environments (development, staging, production) through configuration — not through build tags or conditional compilation. Feature flags allow gradual rollouts without redeployment.

// Environment detection via config
type Environment string

const (
    EnvDevelopment Environment = "development"
    EnvStaging     Environment = "staging"
    EnvProduction  Environment = "production"
)

type Config struct {
    Env          Environment
    Debug        bool
    LogLevel     slog.Level
    // ...
}

func loadConfig() *Config {
    env := Environment(os.Getenv("APP_ENV"))
    if env == "" { env = EnvDevelopment }

    cfg := &Config{Env: env}
    switch env {
    case EnvProduction:
        cfg.LogLevel = slog.LevelInfo
        cfg.Debug = false
    default:
        cfg.LogLevel = slog.LevelDebug
        cfg.Debug = true
    }
    return cfg
}

// Simple feature flag implementation
type FeatureFlags struct {
    mu    sync.RWMutex
    flags map[string]bool
}

func (f *FeatureFlags) IsEnabled(name string) bool {
    f.mu.RLock()
    defer f.mu.RUnlock()
    return f.flags[name]
}

func (f *FeatureFlags) Set(name string, enabled bool) {
    f.mu.Lock()
    defer f.mu.Unlock()
    f.flags[name] = enabled
}

// In a handler:
func userHandler(w http.ResponseWriter, r *http.Request) {
    if flags.IsEnabled("new-user-dashboard") {
        renderNewDashboard(w, r)
        return
    }
    renderLegacyDashboard(w, r)
}
Why is it better to detect the environment via configuration rather than build tags in Go?
What is the primary purpose of a feature flag in a Go service?
24. How do you implement health check and readiness endpoints for a Go service?

Health checks are mandatory for Kubernetes deployments. A liveness probe tells Kubernetes whether the process is running (should it restart?). A readiness probe tells Kubernetes whether the pod should receive traffic (is it ready to serve?).

type HealthChecker struct {
    db    *sql.DB
    cache *redis.Client
    start time.Time
}

func NewHealthChecker(db *sql.DB, cache *redis.Client) *HealthChecker {
    return &HealthChecker{db: db, cache: cache, start: time.Now()}
}

// Liveness: is the process alive? (simple, no dependency checks)
func (h *HealthChecker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, map[string]any{
        "status":   "ok",
        "uptime":   time.Since(h.start).String(),
    })
}

// Readiness: can the service handle requests? (checks dependencies)
func (h *HealthChecker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    type depStatus struct {
        Status string `json:"status"`
        Error  string `json:"error,omitempty"`
    }

    ready := true
    deps := map[string]depStatus{}

    if err := h.db.PingContext(ctx); err != nil {
        deps["database"] = depStatus{Status: "unhealthy", Error: err.Error()}
        ready = false
    } else {
        deps["database"] = depStatus{Status: "healthy"}
    }

    if err := h.cache.Ping(ctx).Err(); err != nil {
        deps["cache"] = depStatus{Status: "unhealthy", Error: err.Error()}
        ready = false
    } else {
        deps["cache"] = depStatus{Status: "healthy"}
    }

    status := http.StatusOK
    if !ready { status = http.StatusServiceUnavailable }
    writeJSON(w, status, map[string]any{
        "ready":       ready,
        "dependencies": deps,
    })
}

// Register
hc := NewHealthChecker(db, cache)
mux.HandleFunc("GET /healthz", hc.LivenessHandler)    // liveness
mux.HandleFunc("GET /readyz",  hc.ReadinessHandler)   // readiness
What is the difference between a liveness probe and a readiness probe in Kubernetes?
Why should the readiness probe use a short context timeout (e.g., 2 seconds)?
25. How do you add observability (metrics and distributed tracing) to a Go service?

Production Go services expose Prometheus metrics and OpenTelemetry traces. Both integrate with Go's standard HTTP server and context-based propagation.

// Prometheus metrics with the standard prometheus/client_golang library
import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "path"},
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal, httpRequestDuration)
}

// Metrics middleware
func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        lrw := &statusResponseWriter{ResponseWriter: w, status: 200}
        next.ServeHTTP(lrw, r)
        dur := time.Since(start).Seconds()
        path := r.URL.Path
        httpRequestsTotal.WithLabelValues(r.Method, path,
            strconv.Itoa(lrw.status)).Inc()
        httpRequestDuration.WithLabelValues(r.Method, path).Observe(dur)
    })
}

// Expose metrics endpoint
mux.Handle("GET /metrics", promhttp.Handler())

// OpenTelemetry tracing (simplified)
// otel.SetTracerProvider(tp)
// tracer := otel.Tracer("myservice")
// ctx, span := tracer.Start(ctx, "operationName")
// defer span.End()
// span.SetAttributes(attribute.String("user.id", userID))
What is the difference between a Counter and a Histogram in Prometheus?
Why should high-cardinality values (like individual user IDs) NOT be used as Prometheus label values?
26. How do build tags work in Go and when do you use them?

Build constraints (build tags) allow you to include or exclude source files from compilation based on OS, architecture, Go version, or custom conditions. They are used for platform-specific code and integration test separation.

// Modern syntax (Go 1.17+) — //go:build directive
// Must be the first non-blank, non-comment line in the file

// OS-specific implementation
// File: signals_unix.go
//go:build linux || darwin

package server

import "syscall"

func shutdownSignals() []os.Signal {
    return []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP}
}

// File: signals_windows.go
//go:build windows

package server

func shutdownSignals() []os.Signal {
    return []os.Signal{os.Interrupt}
}

// Integration test exclusion
// File: integration_test.go
//go:build integration

package api_test

// Run only with: go test -tags integration ./...
func TestDatabaseIntegration(t *testing.T) {
    db := openRealDB(t)
    // ...
}

// Constraint operators:
// //go:build linux && amd64     — AND
// //go:build linux || darwin    — OR
// //go:build !windows           — NOT
// //go:build go1.21             — minimum Go version

// Filename convention (alternative, no build tag needed):
// file_linux.go       — only on Linux
// file_windows_amd64.go — only on Windows/amd64
// file_test.go        — only in test builds
What is the modern syntax for build constraints in Go 1.17+?
How do you run tests that are marked with '//go:build integration'?
27. What are the best practices for using Go's HTTP client in production?

The default http.DefaultClient is not suitable for production — it has no timeouts, uses a shared transport, and cannot be instrumented. Production code creates dedicated clients with explicit configuration.

// Production HTTP client — never use http.DefaultClient in libraries
func newHTTPClient() *http.Client {
    return &http.Client{
        Timeout: 30 * time.Second, // total request timeout including body read
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 10,
            IdleConnTimeout:     90 * time.Second,
            TLSHandshakeTimeout: 10 * time.Second,
            // Keep-alive enabled by default
            DisableKeepAlives: false,
        },
    }
}

// Reuse clients — they manage connection pools internally
var apiClient = newHTTPClient()

// Always use context for cancellation and timeout
func callAPI(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    req.Header.Set("Accept", "application/json")
    req.Header.Set("User-Agent", "myservice/1.0")

    resp, err := apiClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    // Always read and close body — even on error status
    body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) // 10 MB limit
    if err != nil {
        return nil, fmt.Errorf("reading body: %w", err)
    }
    if resp.StatusCode >= 400 {
        return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
    }
    return body, nil
}

// RETRY with exponential backoff for idempotent requests
func callWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
    var lastErr error
    for attempt := 0; attempt < maxRetries; attempt++ {
        if attempt > 0 {
            delay := time.Duration(attempt*attempt) * 100 * time.Millisecond
            select {
            case <-time.After(delay):
            case <-ctx.Done(): return nil, ctx.Err()
            }
        }
        data, err := callAPI(ctx, url)
        if err == nil { return data, nil }
        lastErr = err
    }
    return nil, fmt.Errorf("after %d retries: %w", maxRetries, lastErr)
}
Why should you never use http.DefaultClient in a production Go service?
Why must you always read and close resp.Body even when the status code indicates an error?
28. What patterns make HTTP error handling consistent and DRY in Go?

Go HTTP handlers cannot return errors — the function signature is func(http.ResponseWriter, *http.Request). Several patterns solve this: the custom handler type, the handler error interface, or a response helper pattern.

// Pattern 1: custom handler type that returns error
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error

func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := h(w, r); err != nil {
        handleError(w, r, err)
    }
}

// Typed API errors
type APIError struct {
    Status  int    `json:"-"`
    Code    string `json:"code"`
    Message string `json:"message"`
}
func (e *APIError) Error() string { return e.Message }

// Central error handler
func handleError(w http.ResponseWriter, r *http.Request, err error) {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        writeJSON(w, apiErr.Status, apiErr)
        return
    }
    if errors.Is(err, ErrNotFound) {
        writeJSON(w, http.StatusNotFound,
            &APIError{Code: "NOT_FOUND", Message: "resource not found"})
        return
    }
    // Unknown error — log and return generic 500
    log.Printf("unhandled error %s %s: %v", r.Method, r.URL.Path, err)
    writeJSON(w, http.StatusInternalServerError,
        &APIError{Code: "INTERNAL", Message: "internal server error"})
}

// Handler using the custom type
mux.Handle("GET /users/{id}", HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        return &APIError{Status: 400, Code: "INVALID_ID",
            Message: "id must be an integer"}
    }
    user, err := userService.FindByID(r.Context(), id)
    if err != nil {
        return fmt.Errorf("finding user: %w", err) // wraps ErrNotFound
    }
    return writeJSON(w, http.StatusOK, user)
}))
Why do Go HTTP handlers not return errors, and how does the custom HandlerFunc pattern solve this?
What advantage does a central handleError function provide in a Go API?
29. What are the common API versioning strategies in Go HTTP services?

API versioning allows a service to evolve without breaking existing clients. Go supports several approaches, each with different trade-offs in URL clarity, header complexity, and routing ease.

// Strategy 1: URL path versioning (most common, most explicit)
mux.HandleFunc("GET /v1/users/{id}", v1GetUser)
mux.HandleFunc("GET /v2/users/{id}", v2GetUser)  // new format, new endpoint

// Sub-router pattern for clean version grouping
func v1Routes() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users/{id}", v1GetUser)
    mux.HandleFunc("POST /users", v1CreateUser)
    return mux
}

func v2Routes() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users/{id}", v2GetUser)
    mux.HandleFunc("POST /users", v2CreateUser)
    return mux
}

mainMux := http.NewServeMux()
mainMux.Handle("/v1/", http.StripPrefix("/v1", v1Routes()))
mainMux.Handle("/v2/", http.StripPrefix("/v2", v2Routes()))

// Strategy 2: Accept header versioning (content negotiation)
// Accept: application/vnd.myapi.v2+json
func versionedHandler(w http.ResponseWriter, r *http.Request) {
    accept := r.Header.Get("Accept")
    switch {
    case strings.Contains(accept, "vnd.myapi.v2"):
        v2GetUser(w, r)
    default:
        v1GetUser(w, r)
    }
}

// Strategy 3: Query parameter (least preferred)
// GET /users/1?version=2

// Backward compatibility within a version:
// - Add new optional fields (never remove)
// - Use omitempty on optional response fields
// - Return 422 for unrecognised required fields
// - Treat unknown JSON keys in requests as valid (ignore, don't error)
What is the primary advantage of URL path versioning (e.g. /v1/, /v2/) over Accept header versioning?
What does http.StripPrefix('/v1', handler) do when mounting a versioned sub-router?
30. How do Go programs handle OS signals and interact with the operating system?

Go programs receive OS signals through the os/signal package. Signals are delivered to Go channels via signal.Notify. Common production uses: graceful shutdown (SIGTERM), config reload (SIGHUP), and heap dump triggering (SIGUSR1).

import (
    "os"
    "os/signal"
    "syscall"
)

// Standard graceful shutdown
func waitForShutdown(srv *http.Server, cleanup func()) {
    quit := make(chan os.Signal, 1)
    // Always use a buffered channel — avoid missing a signal if handler is slow
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    defer signal.Stop(quit) // unregister when function exits

    <-quit
    log.Println("shutdown signal received")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
    cleanup()
}

// SIGHUP: reload configuration without restart
func watchConfigReload(cfg *atomic.Pointer[Config]) {
    reload := make(chan os.Signal, 1)
    signal.Notify(reload, syscall.SIGHUP)
    for range reload {
        newCfg, err := loadConfig()
        if err != nil {
            log.Printf("config reload failed: %v", err)
            continue
        }
        cfg.Store(newCfg)
        log.Println("configuration reloaded")
    }
}

// Multiple signals: select over several signal channels
func signalRouter() {
    sigint  := make(chan os.Signal, 1)
    sigusr1 := make(chan os.Signal, 1)
    signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM)
    signal.Notify(sigusr1, syscall.SIGUSR1)

    for {
        select {
        case <-sigint:  initiateShutdown(); return
        case <-sigusr1: dumpHeapProfile()
        }
    }
}
Why should signal channels always be buffered in Go?
What does signal.Stop(ch) do?
31. How do you profile a Go service in production using pprof?

Go ships a built-in profiler accessible via HTTP when you import net/http/pprof. Adding this to a running service provides CPU profiles, heap snapshots, goroutine dumps, and block profiles without restarting the service.

// Add to main or a dedicated debug server
import _ "net/http/pprof"  // side-effect import registers /debug/pprof/ handlers

// Expose on a separate internal port (never public-facing)
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

// Available endpoints:
// /debug/pprof/             — index page
// /debug/pprof/goroutine    — goroutine dump
// /debug/pprof/heap         — heap snapshot
// /debug/pprof/profile?seconds=30 — 30s CPU profile
// /debug/pprof/trace?seconds=5    — execution trace
// /debug/pprof/block        — goroutine blocking
// /debug/pprof/mutex        — mutex contention

// CLI usage:
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
// > top10          — top CPU-consuming functions
// > web            — opens flame graph in browser
// > list funcName  — annotated source code

// Heap profile:
// go tool pprof http://localhost:6060/debug/pprof/heap
// > top10 -cum     — cumulative allocations
// > alloc_space    — total bytes allocated (not just live)
// > inuse_space    — currently live bytes

// Goroutine leak check:
// curl http://localhost:6060/debug/pprof/goroutine?debug=2
// Shows full stack traces of all goroutines

// Programmatic profiling in tests:
// go test -bench=. -cpuprofile=cpu.out -memprofile=mem.out
// go tool pprof cpu.out
What does the blank import '_ "net/http/pprof"' accomplish?
Why should the pprof debug server be exposed on a separate internal port rather than the public API port?
32. How does Go's module system work and what are the key commands?

The Go module system (introduced in Go 1.11, stable in Go 1.13) manages dependencies through a go.mod file. It replaced GOPATH-based dependency management and provides reproducible builds through cryptographic checksums in go.sum.

// go.mod anatomy
module github.com/myorg/myservice

go 1.22

require (
    github.com/lib/pq v1.10.7
    golang.org/x/sync v0.6.0
)

require (
    // Indirect dependencies (transitive)
    github.com/some/dep v1.2.3 // indirect
)

// Key commands:

// Initialise a new module
// go mod init github.com/myorg/myservice

// Add or upgrade a dependency
// go get github.com/lib/pq@v1.10.7
// go get github.com/lib/pq@latest

// Remove unused and add missing dependencies
// go mod tidy

// Verify checksums
// go mod verify

// Vendor dependencies (for reproducible builds or air-gapped environments)
// go mod vendor
// go build -mod=vendor ./...

// Replace directive: use local fork during development
// replace github.com/myorg/lib => ../local-lib

// Minimum Version Selection (MVS):
// Go selects the MINIMUM version of each dependency that satisfies
// all requirements in the module graph — deterministic, no implicit upgrades
// Unlike npm/pip: go get explicitly upgrades, nothing upgrades silently
What does 'go mod tidy' do?
What is Go's Minimum Version Selection (MVS) algorithm?
33. How do you validate HTTP request inputs in Go without a framework?

Input validation is a layered concern: structural validation (is the JSON well-formed?), field validation (are required fields present, within range?), and business validation (does the email already exist?). Go handles this without a framework using explicit checks and helper functions.

// Request and validation error types
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

type ValidationErrors map[string]string
func (ve ValidationErrors) Error() string {
    msgs := make([]string, 0, len(ve))
    for k, v := range ve { msgs = append(msgs, k+": "+v) }
    return strings.Join(msgs, ", ")
}

// Validator function
func validateCreateUser(req CreateUserRequest) error {
    errs := ValidationErrors{}

    if strings.TrimSpace(req.Name) == "" {
        errs["name"] = "is required"
    } else if len(req.Name) > 100 {
        errs["name"] = "must be 100 characters or fewer"
    }

    if req.Email == "" {
        errs["email"] = "is required"
    } else if !isValidEmail(req.Email) {
        errs["email"] = "is not a valid email address"
    }

    if req.Age < 0 || req.Age > 150 {
        errs["age"] = "must be between 0 and 150"
    }

    if len(errs) > 0 { return errs }
    return nil
}

// Handler
func createUserHandler(w http.ResponseWriter, r *http.Request) error {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        return &APIError{Status: 400, Code: "INVALID_JSON",
            Message: "request body must be valid JSON"}
    }

    if err := validateCreateUser(req); err != nil {
        var ve ValidationErrors
        if errors.As(err, &ve) {
            writeJSON(w, http.StatusUnprocessableEntity,
                map[string]any{"errors": ve})
            return nil
        }
        return err
    }

    user, err := userService.Create(r.Context(), req)
    if err != nil { return err }
    return writeJSON(w, http.StatusCreated, user)
}
Why should validation be separated from the handler's business logic?
What HTTP status code should be returned for validation failures (invalid field values)?
34. How do you implement streaming HTTP responses in Go?

Streaming is useful when the response is large, generated incrementally, or delivered in real-time (server-sent events, file downloads). Go's http.Flusher interface allows the handler to push buffered data to the client without waiting for the full response.

// Check if ResponseWriter supports flushing
func streamHandler(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "streaming not supported", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    ctx := r.Context()
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return // client disconnected
        case t := <-ticker.C:
            fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
            flusher.Flush() // push to client immediately
        }
    }
}

// Large file download — stream without loading into memory
func downloadHandler(w http.ResponseWriter, r *http.Request) {
    f, err := os.Open("/data/large-file.csv")
    if err != nil {
        http.Error(w, "file not found", http.StatusNotFound)
        return
    }
    defer f.Close()

    w.Header().Set("Content-Type", "text/csv")
    w.Header().Set("Content-Disposition", `attachment; filename="data.csv"`)

    // io.Copy streams from file to response in 32 KB chunks
    // without loading the entire file into memory
    if _, err := io.Copy(w, f); err != nil {
        log.Printf("download error: %v", err)
    }
}

// NDJSON streaming (newline-delimited JSON for large result sets)
func streamResults(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/x-ndjson")
    enc := json.NewEncoder(w)
    for _, item := range largeResultSet {
        enc.Encode(item)  // writes JSON + newline
        flusher.Flush()
    }
}
What interface must an http.ResponseWriter implement to support Flush()?
Why is io.Copy preferred over reading an entire file into memory and then writing it?
35. How do you write and interpret Go benchmarks?

Go's testing package has built-in benchmark support. Benchmarks are functions with signature func BenchmarkXxx(b *testing.B) and run with go test -bench=.. The framework automatically calibrates the number of iterations.

// Benchmark function
func BenchmarkJSONMarshal(b *testing.B) {
    user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}

    b.ResetTimer() // exclude setup time from measurement
    for i := 0; i < b.N; i++ { // b.N is auto-calibrated
        _, err := json.Marshal(user)
        if err != nil {
            b.Fatal(err)
        }
    }
}

// Memory allocations benchmark
func BenchmarkStringBuilder(b *testing.B) {
    words := []string{"hello", "world", "foo", "bar"}

    b.ReportAllocs() // show allocations in output
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for _, w := range words {
            sb.WriteString(w)
            sb.WriteByte(' ')
        }
        _ = sb.String()
    }
}

// Run benchmarks:
// go test -bench=. -benchmem ./...
// -benchmem: show memory allocations
// -benchtime=5s: run for 5 seconds instead of default 1s
// -count=5: run 5 times for stable results

// Output:
// BenchmarkJSONMarshal-8   1234567   987 ns/op   256 B/op   3 allocs/op
// Columns: name, iterations, ns per op, bytes per op, allocs per op

// Compare benchmarks with benchstat:
// go test -bench=. -count=5 > before.txt
// # make changes
// go test -bench=. -count=5 > after.txt
// benchstat before.txt after.txt  # shows % improvement and p-value
What does b.ResetTimer() do in a Go benchmark?
What does the '-benchmem' flag add to benchmark output?
36. How do you embed static files into a Go binary using go:embed?

//go:embed (Go 1.16) embeds files and directories into the compiled binary at build time. This eliminates the need to distribute static assets separately — the binary is fully self-contained.

import "embed"

// Embed a single file as a string
//go:embed VERSION
var version string

// Embed a single file as bytes
//go:embed config/default.yaml
var defaultConfig []byte

// Embed multiple files as an FS
//go:embed static/*
var staticFiles embed.FS

// Embed entire directory tree (include hidden files with all:)
//go:embed all:templates
var templates embed.FS

// Serving embedded static files
func main() {
    // Serve from embedded FS
    staticFS, err := fs.Sub(staticFiles, "static")
    if err != nil { log.Fatal(err) }

    mux := http.NewServeMux()
    mux.Handle("/static/",
        http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))

    // HTML template from embedded FS
    tmpl := template.Must(
        template.New("").ParseFS(templates, "templates/*.html"))

    mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", map[string]any{
            "Version": version,
        })
    })

    http.ListenAndServe(":8080", mux)
}

// embed.FS implements fs.FS — compatible with all fs.FS consumers
What is the main advantage of using //go:embed for static assets?
What does 'all:templates' in '//go:embed all:templates' do differently from 'templates/*'?
37. How do you implement timeouts for non-HTTP operations like database queries and external calls?

Context-based timeouts apply to any blocking operation — database queries, cache lookups, file operations, and external gRPC calls. The pattern is identical: create a child context with a deadline and pass it to every blocking call.

// Database query with timeout
func getUserFromDB(ctx context.Context, db *sql.DB, id int) (*User, error) {
    // Add query-level timeout (in addition to any HTTP request timeout)
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    row := db.QueryRowContext(ctx,
        "SELECT id, name, email FROM users WHERE id = $1", id)

    var u User
    if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("DB query timed out after 2s: %w", err)
        }
        return nil, fmt.Errorf("getUserFromDB: %w", err)
    }
    return &u, nil
}

// Multiple operations with individual timeouts
func getUserProfile(ctx context.Context, userID int) (*Profile, error) {
    // DB: 2s timeout
    dbCtx, dbCancel := context.WithTimeout(ctx, 2*time.Second)
    defer dbCancel()
    user, err := getUserFromDB(dbCtx, db, userID)
    if err != nil { return nil, err }

    // Cache: 500ms timeout
    cacheCtx, cacheCancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer cacheCancel()
    prefs, _ := getPrefsFromCache(cacheCtx, userID) // non-fatal if cache misses

    // External API: 3s timeout
    apiCtx, apiCancel := context.WithTimeout(ctx, 3*time.Second)
    defer apiCancel()
    avatar, err := fetchAvatarFromCDN(apiCtx, user.AvatarURL)
    if err != nil {
        log.Printf("avatar fetch failed (non-fatal): %v", err)
    }

    return buildProfile(user, prefs, avatar), nil
}
What is the effective deadline when you create a 5-second timeout on a child context whose parent already has a 3-second deadline?
When errors.Is(err, context.DeadlineExceeded) returns true in a database query, what does it indicate?
38. What are the conventions for returning structured error responses from a Go REST API?

A consistent error response format makes APIs predictable for clients. The RFC 7807 (Problem Details for HTTP APIs) standard provides a widely adopted structure. Go implementations typically define a consistent JSON error envelope.

// RFC 7807-inspired error envelope
type ProblemDetail struct {
    Type     string `json:"type"`     // URI identifying the problem type
    Title    string `json:"title"`    // short human-readable summary
    Status   int    `json:"status"`   // HTTP status code
    Detail   string `json:"detail"`   // human-readable explanation
    Instance string `json:"instance"` // URI of the specific occurrence
    // Extension fields
    Errors   map[string]string `json:"errors,omitempty"` // field-level errors
    TraceID  string            `json:"trace_id,omitempty"`
}

// Constructor helpers
func notFound(detail, instance string) *ProblemDetail {
    return &ProblemDetail{
        Type:     "https://api.example.com/errors/not-found",
        Title:    "Resource Not Found",
        Status:   http.StatusNotFound,
        Detail:   detail,
        Instance: instance,
    }
}

func validationError(errs map[string]string) *ProblemDetail {
    return &ProblemDetail{
        Type:   "https://api.example.com/errors/validation",
        Title:  "Validation Failed",
        Status: http.StatusUnprocessableEntity,
        Detail: "One or more fields failed validation",
        Errors: errs,
    }
}

// Middleware: attach trace ID to all error responses
func errorResponseMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/problem+json")
        next.ServeHTTP(w, r)
    })
}

// Usage in handler
user, err := svc.FindUser(r.Context(), id)
if errors.Is(err, ErrNotFound) {
    prob := notFound(
        fmt.Sprintf("user with id %d does not exist", id),
        r.URL.Path,
    )
    w.Header().Set("Content-Type", "application/problem+json")
    w.WriteHeader(prob.Status)
    json.NewEncoder(w).Encode(prob)
    return
}
What Content-Type header should a Problem Detail (RFC 7807) error response use?
What information should be in the 'instance' field of an RFC 7807 Problem Detail?
39. How do you implement pagination for list endpoints in a Go REST API?

Two common pagination strategies: offset-based (page/limit) and cursor-based (token-based). Cursor pagination scales better for large datasets and handles real-time data insertion without the duplicate/skip problems of offset pagination.

// Offset-based pagination (simple, common for small datasets)
type OffsetPagination struct {
    Page    int `json:"page"`     // 1-indexed
    PerPage int `json:"per_page"`
    Total   int `json:"total"`
}

func listUsersHandler(w http.ResponseWriter, r *http.Request) error {
    page    := parseIntParam(r, "page", 1)
    perPage := parseIntParam(r, "per_page", 20)
    if perPage > 100 { perPage = 100 } // cap per-page

    offset := (page - 1) * perPage
    users, total, err := userRepo.List(r.Context(), offset, perPage)
    if err != nil { return err }

    return writeJSON(w, http.StatusOK, map[string]any{
        "data":       users,
        "pagination": OffsetPagination{Page: page, PerPage: perPage, Total: total},
    })
}

// Cursor-based pagination (better for large/live datasets)
type CursorPage struct {
    Data       any    `json:"data"`
    NextCursor string `json:"next_cursor,omitempty"`
    HasMore    bool   `json:"has_more"`
}

func listUsersCursor(w http.ResponseWriter, r *http.Request) error {
    cursor := r.URL.Query().Get("cursor")
    limit  := parseIntParam(r, "limit", 20)

    // Decode opaque cursor (e.g., base64(id+timestamp))
    var afterID int
    if cursor != "" {
        afterID = decodeCursor(cursor)
    }

    users, err := userRepo.ListAfterID(r.Context(), afterID, limit+1)
    if err != nil { return err }

    hasMore := len(users) > limit
    if hasMore { users = users[:limit] }

    var nextCursor string
    if hasMore {
        nextCursor = encodeCursor(users[len(users)-1].ID)
    }

    return writeJSON(w, http.StatusOK, CursorPage{
        Data: users, NextCursor: nextCursor, HasMore: hasMore,
    })
}
What is a key advantage of cursor-based pagination over offset-based pagination?
Why should per-page limits be capped in a list endpoint?
40. How do you implement JWT authentication middleware in Go?

JWT (JSON Web Token) authentication middleware validates the token on every request, extracts claims, and attaches them to the request context for use by downstream handlers.

import "github.com/golang-jwt/jwt/v5"

type Claims struct {
    UserID int    `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

type contextKey string
const claimsKey contextKey = "claims"

func jwtMiddleware(secret []byte) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Extract token from Authorization: Bearer 
            auth := r.Header.Get("Authorization")
            if !strings.HasPrefix(auth, "Bearer ") {
                writeJSON(w, http.StatusUnauthorized,
                    map[string]string{"error": "missing bearer token"})
                return
            }
            tokenString := strings.TrimPrefix(auth, "Bearer ")

            // Parse and validate
            var claims Claims
            token, err := jwt.ParseWithClaims(tokenString, &claims,
                func(token *jwt.Token) (any, error) {
                    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                        return nil, fmt.Errorf("unexpected signing method: %v",
                            token.Header["alg"])
                    }
                    return secret, nil
                })

            if err != nil || !token.Valid {
                writeJSON(w, http.StatusUnauthorized,
                    map[string]string{"error": "invalid or expired token"})
                return
            }

            // Attach claims to context
            ctx := context.WithValue(r.Context(), claimsKey, &claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// Helper to extract claims in handlers
func claimsFromCtx(ctx context.Context) (*Claims, bool) {
    c, ok := ctx.Value(claimsKey).(*Claims)
    return c, ok
}
Why should JWT middleware validate the 'alg' (algorithm) claim before verifying the token?
After JWT middleware validates the token, how does it pass the claims to subsequent handlers?
41. How do you implement HTTP response caching in a Go service?

HTTP caching reduces load and improves response times. Go services implement caching at multiple levels: HTTP Cache-Control headers (browser/CDN caching), application-level caching (Redis/in-memory), and conditional requests (ETag/Last-Modified).

// HTTP Cache-Control headers
func publicDataHandler(w http.ResponseWriter, r *http.Request) {
    // Cache in browser and CDN for 60s, stale for 10s
    w.Header().Set("Cache-Control", "public, max-age=60, stale-while-revalidate=10")
    w.Header().Set("Vary", "Accept-Encoding") // vary by encoding
    // ... serve data
}

func privateDataHandler(w http.ResponseWriter, r *http.Request) {
    // No CDN caching — only browser may cache, private to user
    w.Header().Set("Cache-Control", "private, max-age=30")
    // ... serve user-specific data
}

// ETag-based conditional caching
func userHandler(w http.ResponseWriter, r *http.Request) {
    user, err := svc.GetUser(r.Context(), r.PathValue("id"))
    if err != nil { /* handle error */ return }

    // Compute ETag (hash of the content)
    data, _ := json.Marshal(user)
    etag := fmt.Sprintf(`"%x"`, md5.Sum(data))

    w.Header().Set("ETag", etag)
    w.Header().Set("Cache-Control", "private, must-revalidate")

    // Check if client has current version
    if r.Header.Get("If-None-Match") == etag {
        w.WriteHeader(http.StatusNotModified) // 304: send no body
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(data)
}

// In-memory LRU cache middleware
func cacheMiddleware(cache *lru.Cache, ttl time.Duration) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Method != http.MethodGet { next.ServeHTTP(w, r); return }
            key := r.URL.RequestURI()
            if val, ok := cache.Get(key); ok {
                w.Header().Set("X-Cache", "HIT")
                w.Write(val.([]byte)); return
            }
            crw := &capturingResponseWriter{ResponseWriter: w}
            next.ServeHTTP(crw, r)
            cache.Add(key, crw.body)
        })
    }
}
What HTTP status code should be returned when an If-None-Match request matches the current ETag?
What does 'Cache-Control: stale-while-revalidate=10' tell a CDN?
42. How does gRPC work in Go and when would you choose it over REST/JSON?

gRPC is a high-performance RPC framework using Protocol Buffers (binary serialisation) over HTTP/2. Go has first-class gRPC support through google.golang.org/grpc. It is the standard for inter-service communication in Go microservices.

// user.proto defines the service contract
// service UserService {
//   rpc GetUser(GetUserRequest) returns (User);
//   rpc StreamUsers(Empty) returns (stream User);
// }

// Generated Go code — implement the server interface
type userServiceServer struct {
    pb.UnimplementedUserServiceServer // embed for forward compatibility
    repo UserRepository
}

func (s *userServiceServer) GetUser(
    ctx context.Context,
    req *pb.GetUserRequest,
) (*pb.User, error) {
    user, err := s.repo.FindByID(ctx, int(req.Id))
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
        }
        return nil, status.Errorf(codes.Internal, "internal error: %v", err)
    }
    return &pb.User{Id: int64(user.ID), Name: user.Name, Email: user.Email}, nil
}

// Start gRPC server
func main() {
    lis, err := net.Listen("tcp", ":9090")
    if err != nil { log.Fatal(err) }

    grpcServer := grpc.NewServer(
        grpc.ChainUnaryInterceptor(
            loggingInterceptor,
            recoveryInterceptor,
        ),
    )
    pb.RegisterUserServiceServer(grpcServer, &userServiceServer{repo: repo})
    grpcServer.Serve(lis)
}
REST/JSON vs gRPC
AspectREST/JSONgRPC/Protobuf
SerialisationJSON (text, ~30 bytes/field)Protobuf (binary, ~3 bytes/field)
Browser supportNativeRequires grpc-web proxy
Code generationOptionalRequired (protoc)
StreamingSSE or WebSocket workaroundsBuilt-in bidirectional streaming
Best forPublic APIs, browser clientsInternal service-to-service RPC
What does 'pb.UnimplementedUserServiceServer' embedding provide?
How do you return a 'not found' error in a gRPC handler?
43. What linters and static analysis tools are essential for production Go code quality?

Go has excellent static analysis tooling built into the ecosystem. Combining multiple linters via golangci-lint catches common bugs, style violations, and security issues before code review.

Key Go Linters and Tools
ToolPurpose
go vetBuilt-in: detects suspicious code (Printf format mismatches, unreachable code, mutex copies)
staticcheckAdvanced bug detection: deprecated API use, logic errors go vet misses
errcheckEnsures error return values are not silently ignored
golangci-lintMeta-linter runner: runs 50+ linters with one command and config
govulncheckSecurity: finds known CVEs in dependencies
gofmt / goimportsCode formatting and import organisation — non-negotiable in CI
goleakTest helper: detects goroutine leaks after test completion
shadowDetects variable shadowing that can hide bugs
# .golangci.yml — typical production configuration
linters:
  enable:
    - govet          # static analysis
    - errcheck       # unchecked errors
    - staticcheck    # advanced analysis
    - gosec          # security issues
    - misspell       # spelling errors in comments/strings
    - gofmt          # formatting
    - goimports      # import ordering
    - revive         # style and best practices
    - prealloc       # suggest slice pre-allocation
    - noctx          # HTTP requests without context

linters-settings:
  errcheck:
    check-type-assertions: true  # flag unchecked type assertions too

# Run:
# golangci-lint run ./...
# govulncheck ./...

# CI pipeline minimum:
# 1. go build ./...
# 2. go vet ./...
# 3. golangci-lint run ./...
# 4. go test -race -count=1 ./...
# 5. govulncheck ./...
What does the 'errcheck' linter detect?
What does govulncheck do in a Go project?
44. What is the recommended project structure for a production Go service?

Go does not mandate a project layout, but the community has converged on a practical structure that separates concerns without over-engineering. The key principle: packages should be named for what they contain, not what they do.

myservice/
├── cmd/
│   └── server/
│       └── main.go          # entry point — only wires dependencies
├── internal/                 # private packages — not importable by other modules
│   ├── api/                  # HTTP handlers and middleware
│   │   ├── handler_user.go
│   │   └── middleware.go
│   ├── domain/               # business logic — no infrastructure dependencies
│   │   ├── user.go           # User type, UserService interface
│   │   └── user_service.go   # UserService implementation
│   ├── store/                # data access layer
│   │   ├── postgres/
│   │   │   └── user_store.go # postgres implementation of UserRepository
│   │   └── memory/
│   │       └── user_store.go # in-memory implementation for tests
│   └── config/
│       └── config.go         # configuration loading and validation
├── pkg/                      # reusable packages (can be imported externally)
│   └── httputil/
│       └── response.go       # writeJSON, writeError helpers
├── migrations/               # database migration SQL files
├── docker/                   # Dockerfile, docker-compose.yml
├── go.mod
├── go.sum
├── Makefile                  # build, test, lint targets
└── README.md

// Key decisions:
// internal/ prevents external packages from importing your internal logic
// cmd/ can have multiple binaries (server, worker, cli)
// domain/ has NO imports from store/ or api/ — dependency flows inward
// Tests live alongside the code they test (*_test.go files)
// main.go is thin — create, connect, start; no business logic
What is the purpose of the 'internal/' directory in a Go module?
Why should the domain/business logic layer have no imports from the data access or HTTP layers?
45. How do you implement load shedding and request queue limits in a Go HTTP server?

Load shedding protects a service from cascading failure under overload: when the system cannot keep up, it deliberately rejects additional requests rather than slowing down and failing all requests. Go implements this through semaphores and queue limits on the HTTP layer.

// Concurrency limiter: max N requests processed simultaneously
type concurrencyLimiter struct {
    sem chan struct{}
}

func newConcurrencyLimiter(max int) *concurrencyLimiter {
    return &concurrencyLimiter{sem: make(chan struct{}, max)}
}

func (l *concurrencyLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        select {
        case l.sem <- struct{}{}: // acquire slot
        default:
            // Immediately reject if at capacity — no waiting
            w.Header().Set("Retry-After", "1")
            http.Error(w, "server busy — try again later",
                http.StatusServiceUnavailable)
            return
        }
        defer func() { <-l.sem }() // release slot
        next.ServeHTTP(w, r)
    })
}

// Request queue with timeout
type queuedLimiter struct {
    queue   chan struct{}
    timeout time.Duration
}

func (l *queuedLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), l.timeout)
        defer cancel()

        select {
        case l.queue <- struct{}{}: // wait up to timeout for a slot
        case <-ctx.Done():
            http.Error(w, "request queue full",
                http.StatusServiceUnavailable)
            return
        }
        defer func() { <-l.queue }()
        next.ServeHTTP(w, r)
    })
}

// Wire up
limiter := newConcurrencyLimiter(100) // max 100 concurrent requests
handler := limiter.Middleware(rateLimiter.Middleware(mux))
What HTTP status code should be returned when load shedding rejects a request?
What is the key difference between a rate limiter and a concurrency limiter?
46. How do you correctly propagate errors from concurrent goroutines in a Go service?

When multiple goroutines run in parallel, errors must be collected and propagated without data races or goroutine leaks. The idiomatic tools are errgroup for structured concurrency and buffered channels for ad-hoc patterns.

import "golang.org/x/sync/errgroup"

// Pattern 1: errgroup — run N tasks, fail fast on first error
func fetchUserProfile(ctx context.Context, userID int) (*Profile, error) {
    g, ctx := errgroup.WithContext(ctx)
    // gctx cancelled when any goroutine fails or g.Wait() returns

    var user *User
    g.Go(func() error {
        var err error
        user, err = userRepo.FindByID(ctx, userID)
        return fmt.Errorf("fetching user: %w", err)
    })

    var orders []Order
    g.Go(func() error {
        var err error
        orders, err = orderRepo.ListByUser(ctx, userID)
        return fmt.Errorf("fetching orders: %w", err)
    })

    if err := g.Wait(); err != nil {
        return nil, err // first error from any goroutine
    }
    return buildProfile(user, orders), nil
}

// Pattern 2: collect ALL errors (don't fail fast)
func validateItems(ctx context.Context, items []Item) error {
    errs := make([]error, len(items))
    var wg sync.WaitGroup
    wg.Add(len(items))

    for i, item := range items {
        go func(idx int, it Item) {
            defer wg.Done()
            errs[idx] = validateItem(ctx, it) // each goroutine writes its own slot
        }(i, item)
    }
    wg.Wait()

    // Filter nil errors and join
    var nonNil []error
    for _, e := range errs { if e != nil { nonNil = append(nonNil, e) } }
    return errors.Join(nonNil...)
}

// errgroup with concurrency limit (Go 1.20+)
g.SetLimit(10)  // max 10 goroutines running at once
for _, item := range items {
    item := item
    g.Go(func() error { return process(ctx, item) })
}
What does errgroup.WithContext(ctx) return in addition to the group?
In the validateItems pattern, why is it safe to write 'errs[idx] = err' from multiple goroutines without a mutex?
47. How do you document a Go REST API and maintain an OpenAPI specification?

Documentation is a first-class concern for production APIs. Go projects adopt either code-first (generate OpenAPI from code annotations) or design-first (write OpenAPI spec, generate server stubs) approaches.

// Approach 1: code-first with swaggo/swag annotations
// go install github.com/swaggo/swag/cmd/swag@latest
// swag init -g cmd/server/main.go  → generates docs/swagger.json

// @Summary Get user by ID
// @Description Returns a single user by their unique identifier
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} User
// @Failure 404 {object} ProblemDetail
// @Failure 500 {object} ProblemDetail
// @Router /users/{id} [get]
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    // handler implementation
}

// Approach 2: design-first with oapi-codegen
// Write openapi.yaml first, then generate:
// oapi-codegen -package api -generate types,server openapi.yaml > api/api.gen.go

// Generated interface — implement this in your handler
type StrictServerInterface interface {
    GetUser(ctx context.Context, request GetUserRequestObject) (GetUserResponseObject, error)
    CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error)
}

// Serve Swagger UI and spec
mux.Handle("GET /docs/",
    http.StripPrefix("/docs/", swaggerui.Handler("openapi.yaml")))
mux.Handle("GET /openapi.yaml", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "openapi.yaml")
})

Design-first advantages: the API contract is established before implementation — frontend and backend teams can work in parallel. Type safety is enforced by generated code. Breaking changes are visible in spec diffs before any code runs.

What is the main advantage of the design-first (OpenAPI spec → code generation) approach?
What does 'swag init' do in a Go project?
48. What is the production readiness checklist for a Go HTTP service?

This summary covers the essential checks interviewers probe when asking 'is this service production-ready?' It combines all the topics from this set into a concise reference.

Production Readiness Checklist
CategoryRequirement
Error handlingAll errors wrapped with %w; errors.Is/As used for inspection; typed nil trap avoided
Contextcontext.Context propagated to all blocking operations; cancel always deferred
HTTP serverReadTimeout, WriteTimeout, IdleTimeout configured; graceful shutdown implemented
MiddlewareRecovery, logging, auth, metrics, rate limiting applied in correct order
LoggingStructured logging (slog) with request IDs; no sensitive data logged
MetricsPrometheus /metrics endpoint; request count, latency, error rate tracked
Health checks/healthz (liveness) and /readyz (readiness) endpoints
ConfigurationLoaded and validated at startup; no os.Getenv in business logic
SecurityJWT validated (alg checked); input sanitised; CORS configured
DatabaseConnection pool configured; all queries use context; rows.Close deferred
Graceful shutdownSIGTERM handled; in-flight requests drained; connections closed
TestingTable-driven tests; -race flag in CI; goleak in goroutine tests
Profilingpprof on internal port; not public-facing
Lintinggolangci-lint + govulncheck in CI pipeline
// One-line checklist for interview answers:

// 1. Errors: always wrap (%w), never swallow, typed nil trap avoided
// 2. Context: propagate to all I/O, defer cancel(), use r.Context() in handlers
// 3. HTTP server: set all timeouts, graceful shutdown on SIGTERM
// 4. Middleware chain: recovery → logging → auth → rate limiting → handler
// 5. Configuration: load + validate at startup, inject as struct
// 6. Health: /healthz (alive?) + /readyz (ready?) separate endpoints
// 7. Observability: structured logs + Prometheus metrics + pprof
// 8. Security: validate JWT alg, sanitise inputs, whitelist CORS origins
// 9. Testing: table-driven, -race, httptest.NewRecorder, mock via interfaces
// 10. CI: vet + golangci-lint + govulncheck + go test -race
Which timeout in http.Server prevents a slow client from holding a connection open while slowly sending a large request body?
In the middleware chain 'recovery → logging → auth → handler', why must recovery be the outermost middleware?
«
»

Comments & Discussions