GoLang Microservices: Patterns from Building 12 Production Services
Battle-tested Go patterns for microservices — context propagation, graceful shutdown, structured logging, circuit breakers, gRPC + HTTP duality, and the boring infrastructure that wins.
Why Go for Microservices
After shipping 12 Go services in production at Hureka, the pattern is clear: Go's simplicity, single binary deployment, and goroutine concurrency make it ideal for I/O-bound services that need to be cheap to run and easy to reason about.
Service Skeleton
package mainimport ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" )
func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger)
srv := &http.Server{ Addr: ":" + getEnv("PORT", "8080"), Handler: buildRouter(), ReadTimeout: 5 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, }
go func() { slog.Info("server starting", "addr", srv.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("server failed", "err", err) os.Exit(1) } }()
quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit
slog.Info("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { slog.Error("graceful shutdown failed", "err", err) } } ```
Context Propagation Everywhere
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()// Propagate request_id from headers if rid := r.Header.Get("X-Request-ID"); rid != "" { ctx = context.WithValue(ctx, requestIDKey, rid) }
result, err := h.service.Process(ctx, payload) if err != nil { if errors.Is(err, context.DeadlineExceeded) { http.Error(w, "timeout", http.StatusGatewayTimeout) return } http.Error(w, "internal", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(result) } ```
Circuit Breaker for External Calls
import "github.com/sony/gobreaker/v2"cb := gobreaker.NewCircuitBreaker[Response](gobreaker.Settings{ Name: "billing-api", MaxRequests: 5, Interval: 30 * time.Second, Timeout: 60 * time.Second, ReadyToTrip: func(c gobreaker.Counts) bool { return c.ConsecutiveFailures > 5 }, })
result, err := cb.Execute(func() (Response, error) { return billingClient.Charge(ctx, req) }) ```
Structured Logging
slog.InfoContext(ctx, "payment processed",
"amount", req.Amount,
"currency", req.Currency,
"tenant_id", tenantID,
"duration_ms", time.Since(start).Milliseconds(),
)
gRPC + HTTP from the Same Process
import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"mux := runtime.NewServeMux() pb.RegisterUserServiceHandlerServer(ctx, mux, userServer)
mainMux := http.NewServeMux() mainMux.Handle("/api/", mux) // REST via gateway mainMux.HandleFunc("/health", healthHandler)
http.ListenAndServe(":8080", mainMux) go grpcServer.Serve(grpcListener) ```
Operational Wins
- 1Statically linked binary — One file, no runtime, fits in a 20MB distroless image
- 2Built-in pprof — `net/http/pprof` for live CPU/memory profiling in production
- 3Cheap goroutines — Spawn freely; a goroutine is ~2KB
- 4Predictable GC — No long pauses; Go's GC is sub-millisecond at typical loads