Production Best Practices
Building production-ready Go applications requires more than just functional code. This chapter covers logging, configuration, deployment, and operational concerns.Structured Logging
Using slog (Go 1.21+)
Go 1.21 introducedlog/slog for structured logging.
Copy
import "log/slog"
func main() {
// JSON logger for production
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
}))
slog.SetDefault(logger)
// Basic logging
slog.Info("Server starting", "port", 8080)
slog.Error("Failed to connect", "error", err, "host", "localhost")
// With context
logger := slog.With("service", "user-service", "version", "1.0.0")
logger.Info("Request received", "method", "GET", "path", "/users")
}
Custom Logger with Context
Copy
type contextKey string
const loggerKey contextKey = "logger"
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}
func LoggerFromContext(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return logger
}
return slog.Default()
}
// Middleware to add request-scoped logger
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := uuid.New().String()
logger := slog.With(
"request_id", requestID,
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
)
ctx := WithLogger(r.Context(), logger)
r = r.WithContext(ctx)
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(wrapped, r)
logger.Info("Request completed",
"status", wrapped.statusCode,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
Using Zap (High Performance)
Copy
import "go.uber.org/zap"
func NewLogger(env string) (*zap.Logger, error) {
var config zap.Config
if env == "production" {
config = zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
} else {
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
return config.Build()
}
func main() {
logger, _ := NewLogger("production")
defer logger.Sync()
// Use sugar for printf-style
sugar := logger.Sugar()
sugar.Infow("Server starting",
"port", 8080,
"env", "production",
)
// Or structured logger
logger.Info("Request received",
zap.String("method", "GET"),
zap.String("path", "/users"),
zap.Duration("latency", time.Millisecond*50),
)
}
Configuration Management
Using Environment Variables
Copy
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Logger LoggerConfig
}
type ServerConfig struct {
Host string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
Port int `env:"SERVER_PORT" envDefault:"8080"`
ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT" envDefault:"5s"`
WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT" envDefault:"10s"`
}
type DatabaseConfig struct {
Host string `env:"DB_HOST" envDefault:"localhost"`
Port int `env:"DB_PORT" envDefault:"5432"`
User string `env:"DB_USER,required"`
Password string `env:"DB_PASSWORD,required"`
Name string `env:"DB_NAME,required"`
SSLMode string `env:"DB_SSLMODE" envDefault:"disable"`
}
// Using envconfig
import "github.com/caarlos0/env/v9"
func LoadConfig() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Using Viper (Full-Featured)
Copy
import "github.com/spf13/viper"
func LoadConfig(path string) (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(path)
viper.AddConfigPath(".")
// Environment variable override
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Set defaults
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.read_timeout", "5s")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, err
}
// Config file not found, use defaults and env vars
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
Config File Example
Copy
# config.yaml
server:
host: "0.0.0.0"
port: 8080
read_timeout: "5s"
write_timeout: "10s"
database:
host: "localhost"
port: 5432
user: "postgres"
name: "myapp"
ssl_mode: "disable"
redis:
host: "localhost"
port: 6379
db: 0
logger:
level: "info"
format: "json"
Graceful Shutdown
Copy
func main() {
cfg := LoadConfig()
// Initialize dependencies
db := initDatabase(cfg.Database)
defer db.Close()
cache := initRedis(cfg.Redis)
defer cache.Close()
// Create server
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
Handler: setupRoutes(db, cache),
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
// Start server in goroutine
go func() {
slog.Info("Server starting", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("Server error", "error", err)
os.Exit(1)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("Shutting down server...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown order matters!
// 1. Stop accepting new requests
if err := srv.Shutdown(ctx); err != nil {
slog.Error("Server shutdown error", "error", err)
}
// 2. Close database connections
if err := db.Close(); err != nil {
slog.Error("Database close error", "error", err)
}
// 3. Close cache connections
if err := cache.Close(); err != nil {
slog.Error("Cache close error", "error", err)
}
slog.Info("Server stopped")
}
Health Checks
Copy
type HealthChecker struct {
db *sql.DB
redis *redis.Client
}
type HealthStatus struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Services map[string]string `json:"services"`
}
func (h *HealthChecker) Check(ctx context.Context) *HealthStatus {
status := &HealthStatus{
Status: "healthy",
Timestamp: time.Now(),
Services: make(map[string]string),
}
// Check database
if err := h.db.PingContext(ctx); err != nil {
status.Status = "unhealthy"
status.Services["database"] = err.Error()
} else {
status.Services["database"] = "ok"
}
// Check Redis
if err := h.redis.Ping(ctx).Err(); err != nil {
status.Status = "unhealthy"
status.Services["redis"] = err.Error()
} else {
status.Services["redis"] = "ok"
}
return status
}
// Endpoints
func (h *HealthChecker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func (h *HealthChecker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
status := h.Check(ctx)
w.Header().Set("Content-Type", "application/json")
if status.Status != "healthy" {
w.WriteHeader(http.StatusServiceUnavailable)
}
json.NewEncoder(w).Encode(status)
}
Metrics with Prometheus
Copy
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"},
)
dbQueryDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query duration in seconds",
Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1},
},
[]string{"query"},
)
activeConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "active_connections",
Help: "Number of active connections",
},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
prometheus.MustRegister(dbQueryDuration)
prometheus.MustRegister(activeConnections)
}
// Middleware
func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(wrapped, r)
duration := time.Since(start).Seconds()
path := r.URL.Path
httpRequestsTotal.WithLabelValues(r.Method, path, fmt.Sprint(wrapped.statusCode)).Inc()
httpRequestDuration.WithLabelValues(r.Method, path).Observe(duration)
})
}
func main() {
mux := http.NewServeMux()
// Your routes
mux.Handle("/api/", apiHandler)
// Metrics endpoint
mux.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", MetricsMiddleware(mux))
}
Docker Deployment
Dockerfile
Copy
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server
# Final stage
FROM alpine:3.18
# Add CA certificates for HTTPS
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
# Copy binary
COPY --from=builder /app/server .
COPY --from=builder /app/config ./config
# Create non-root user
RUN adduser -D -g '' appuser
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1
ENTRYPOINT ["./server"]
Docker Compose
Copy
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SERVER_PORT=8080
- DB_HOST=postgres
- DB_USER=postgres
- DB_PASSWORD=secret
- DB_NAME=myapp
- REDIS_HOST=redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:8080/health/live"]
interval: 10s
timeout: 5s
retries: 3
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
Kubernetes Deployment
Copy
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: myapp-config
key: db_host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db_password
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Build and Version Info
Copy
// Injected at build time
var (
Version = "dev"
GitCommit = "unknown"
BuildTime = "unknown"
)
// Build with:
// go build -ldflags "-X main.Version=1.0.0 -X main.GitCommit=$(git rev-parse HEAD) -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
func main() {
slog.Info("Starting application",
"version", Version,
"commit", GitCommit,
"build_time", BuildTime,
)
}
// Version endpoint
func versionHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{
"version": Version,
"git_commit": GitCommit,
"build_time": BuildTime,
})
}
Makefile
Copy
VERSION ?= $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse HEAD)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.GitCommit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME)"
.PHONY: build
build:
go build $(LDFLAGS) -o bin/server ./cmd/server
.PHONY: test
test:
go test -v -race -cover ./...
.PHONY: lint
lint:
golangci-lint run
.PHONY: docker
docker:
docker build -t myapp:$(VERSION) .
.PHONY: run
run: build
./bin/server
Security Best Practices
Copy
// Secure HTTP server
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
}
// Security headers middleware
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
// Rate limiting per IP
func PerIPRateLimiter() func(http.Handler) http.Handler {
type client struct {
limiter *rate.Limiter
lastSeen time.Time
}
var (
mu sync.Mutex
clients = make(map[string]*client)
)
// Cleanup old entries
go func() {
for {
time.Sleep(time.Minute)
mu.Lock()
for ip, c := range clients {
if time.Since(c.lastSeen) > 3*time.Minute {
delete(clients, ip)
}
}
mu.Unlock()
}
}()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getIP(r)
mu.Lock()
c, exists := clients[ip]
if !exists {
c = &client{limiter: rate.NewLimiter(10, 20)} // 10 req/s, burst 20
clients[ip] = c
}
c.lastSeen = time.Now()
mu.Unlock()
if !c.limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
Interview Questions
How do you handle configuration in production Go applications?
How do you handle configuration in production Go applications?
- Use environment variables for secrets and deployment-specific values
- Use config files (YAML/JSON) for application defaults
- Libraries like Viper for unified configuration
- Never commit secrets to version control
- Support configuration hot-reloading for non-sensitive values
What should happen during graceful shutdown?
What should happen during graceful shutdown?
- Stop accepting new connections
- Wait for in-flight requests to complete (with timeout)
- Close database connections
- Close cache connections
- Flush logs and metrics
- Exit cleanly
How do you implement health checks?
How do you implement health checks?
Two types:
- Liveness: Is the process running? Simple 200 OK response
- Readiness: Can the service handle traffic? Check dependencies (DB, cache, etc.)
What metrics should you collect for a Go service?
What metrics should you collect for a Go service?
- HTTP request count/rate by status code
- Request latency (histogram)
- Active connections
- Database query latency
- Cache hit/miss rate
- Goroutine count
- Memory usage
- Error rates
Summary
| Area | Key Points |
|---|---|
| Logging | Structured (slog/zap), request IDs, context propagation |
| Configuration | Env vars + files, secrets management, validation |
| Shutdown | Graceful, proper ordering, timeouts |
| Health Checks | Liveness + readiness probes |
| Metrics | Prometheus, business + technical metrics |
| Deployment | Multi-stage Docker, K8s manifests |
| Security | Headers, rate limiting, input validation |