Go Pro Tips — Production Go Applications
Patterns, anti-patterns, and core principles that Go senior developers learn from real production experience. Master the idiomatic way to write Go.
Advanced Error Handling
Sentinel Errors vs Custom Error Types
package main
import (
"errors"
"fmt"
)
// 1. Sentinel errors — comparable error values
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
// 2. Custom error types — include additional context
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: field=%s, value=%v, message=%s",
e.Field, e.Value, e.Message)
}
// 3. Error wrapping — preserve context
func getUser(id int) error {
if id <= 0 {
return fmt.Errorf("getUser: %w", &ValidationError{
Field: "id",
Value: id,
Message: "must be positive",
})
}
// Simulate DB lookup failure
return fmt.Errorf("getUser: DB lookup failed (id=%d): %w", id, ErrNotFound)
}
func processUser(id int) error {
if err := getUser(id); err != nil {
return fmt.Errorf("processUser: %w", err) // Stack-like wrapping
}
return nil
}
func main() {
err := processUser(-1)
if err != nil {
fmt.Println("Error:", err)
// errors.Is: search error chain for sentinel error
if errors.Is(err, ErrNotFound) {
fmt.Println("→ Resource not found")
}
// errors.As: search error chain for type
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("→ Validation error: field=%s\n", valErr.Field)
}
}
}
Error Groups — Collect Errors from Multiple Goroutines
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func fetchData(ctx context.Context, url string) (string, error) {
// Simulate HTTP request
if url == "" {
return "", fmt.Errorf("empty URL")
}
return "data from " + url, nil
}
func main() {
ctx := context.Background()
// errgroup: handle multiple goroutine errors at once
g, ctx := errgroup.WithContext(ctx)
urls := []string{
"https://api1.example.com",
"https://api2.example.com",
"", // Error case
}
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // Capture loop variables
g.Go(func() error {
data, err := fetchData(ctx, url)
if err != nil {
return fmt.Errorf("URL[%d] failed: %w", i, err)
}
results[i] = data
return nil
})
}
// Wait for all goroutines — returns first error
if err := g.Wait(); err != nil {
fmt.Println("Error occurred:", err)
return
}
fmt.Println("All results:", results)
}
Context Propagation Pattern
package main
import (
"context"
"fmt"
"time"
)
// Context key type — prevents collisions
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
// Middleware: inject values into context
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func withUserID(ctx context.Context, id int64) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
// Helper functions for type-safe extraction
func getRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return "unknown"
}
func getUserID(ctx context.Context) int64 {
if id, ok := ctx.Value(userIDKey).(int64); ok {
return id
}
return 0
}
// Context cancellation propagation
func longRunningTask(ctx context.Context) error {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled or context.DeadlineExceeded
default:
fmt.Printf("[reqID=%s] Progress: %d/10\n",
getRequestID(ctx), i+1)
time.Sleep(100 * time.Millisecond)
}
}
return nil
}
func main() {
// Compose request context
ctx := context.Background()
ctx = withRequestID(ctx, "req-12345")
ctx = withUserID(ctx, 42)
// Add timeout
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
if err := longRunningTask(ctx); err != nil {
fmt.Printf("Task cancelled: %v\n", err) // context deadline exceeded
}
}
Interface Design Principles
package main
import (
"fmt"
"io"
"os"
"strings"
)
// Go interface golden rule: small, single-responsibility
// Bad: oversized interface
type BadStorage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List() ([]string, error)
Flush() error
Stats() map[string]int
Close() error
Ping() error
}
// Good: small composable interfaces
type Getter interface {
Get(key string) ([]byte, error)
}
type Setter interface {
Set(key string, val []byte) error
}
type ReadWriter interface {
Getter
Setter
}
// Follow standard library like io.Reader
func processInput(r io.Reader) (int, error) {
buf := make([]byte, 1024)
total := 0
for {
n, err := r.Read(buf)
total += n
if err == io.EOF {
break
}
if err != nil {
return total, err
}
}
return total, nil
}
func main() {
// Unified by io.Reader: files, strings, networks, etc.
fileReader, _ := os.Open("go.mod")
defer fileReader.Close()
n1, _ := processInput(fileReader)
fmt.Printf("Read %d bytes from file\n", n1)
stringReader := strings.NewReader("hello world")
n2, _ := processInput(stringReader)
fmt.Printf("Read %d bytes from string\n", n2)
}
Advanced Concurrency Patterns
Fan-Out / Fan-In Pipeline
package main
import (
"fmt"
"sync"
)
// Generator — channel source
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
// Stage — transformation
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
// Fan-Out — distribute to multiple workers
func fanOut(in <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = square(in)
}
return channels
}
// Fan-In — merge multiple channels
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range channels {
ch := ch
wg.Add(1)
go func() {
defer wg.Done()
for v := range ch {
out <- v
}
}()
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
// Pipeline: generate → Fan-Out(4 workers) → Fan-In → output
nums := generate(1, 2, 3, 4, 5, 6, 7, 8)
results := fanIn(fanOut(nums, 4)...)
for v := range results {
fmt.Printf("%d ", v)
}
fmt.Println()
}
Done Channel Pattern (Cancellation)
package main
import (
"fmt"
"time"
)
// Cancellable worker
func worker(done <-chan struct{}, id int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 0; ; i++ {
select {
case <-done:
fmt.Printf("Worker %d shutting down\n", id)
return
case out <- i:
time.Sleep(10 * time.Millisecond)
}
}
}()
return out
}
func main() {
done := make(chan struct{})
w1 := worker(done, 1)
w2 := worker(done, 2)
// Collect results for 300ms
timeout := time.After(300 * time.Millisecond)
for {
select {
case v := <-w1:
fmt.Printf("w1: %d\n", v)
case v := <-w2:
fmt.Printf("w2: %d\n", v)
case <-timeout:
close(done) // Cancel all workers
time.Sleep(50 * time.Millisecond) // Cleanup wait
fmt.Println("Done")
return
}
}
}
Configuration Management Best Practices
// config/config.go
package config
import (
"fmt"
"os"
"strconv"
"time"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Cache CacheConfig
}
type ServerConfig struct {
Host string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type DatabaseConfig struct {
DSN string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}
type CacheConfig struct {
RedisURL string
TTL time.Duration
}
// Load from environment variables (with defaults)
func Load() (*Config, error) {
cfg := &Config{
Server: ServerConfig{
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Port: getEnvInt("SERVER_PORT", 8080),
ReadTimeout: getEnvDuration("SERVER_READ_TIMEOUT", 15*time.Second),
WriteTimeout: getEnvDuration("SERVER_WRITE_TIMEOUT", 15*time.Second),
},
Database: DatabaseConfig{
DSN: mustGetEnv("DATABASE_URL"),
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
ConnMaxLifetime: getEnvDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
},
Cache: CacheConfig{
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
TTL: getEnvDuration("CACHE_TTL", 5*time.Minute),
},
}
return cfg, nil
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
func mustGetEnv(key string) string {
val := os.Getenv(key)
if val == "" {
panic(fmt.Sprintf("required environment variable missing: %s", key))
}
return val
}
func getEnvInt(key string, defaultVal int) int {
if val := os.Getenv(key); val != "" {
if n, err := strconv.Atoi(val); err == nil {
return n
}
}
return defaultVal
}
func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
if val := os.Getenv(key); val != "" {
if d, err := time.ParseDuration(val); err == nil {
return d
}
}
return defaultVal
}
Structured Logging — slog (Go 1.21+)
package main
import (
"context"
"log/slog"
"os"
"time"
)
func setupLogger(env string) *slog.Logger {
var handler slog.Handler
if env == "production" {
// JSON format (log aggregator friendly)
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
} else {
// Text format (human-readable for development)
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true, // Include file:line
})
}
return slog.New(handler)
}
func main() {
logger := setupLogger("development")
// Set as default logger
slog.SetDefault(logger)
// Basic logging
slog.Info("server starting", "port", 8080, "env", "development")
slog.Debug("debug info", "goroutines", 10)
slog.Warn("slow query", "duration_ms", 250, "query", "SELECT *")
slog.Error("DB connection failed", "error", "connection refused", "retry", 3)
// Structured groups
slog.Info("request handled",
slog.Group("request",
slog.String("method", "GET"),
slog.String("path", "/users/123"),
slog.String("ip", "192.168.1.1"),
),
slog.Group("response",
slog.Int("status", 200),
slog.Duration("duration", 45*time.Millisecond),
),
)
// With context (propagate request ID)
ctx := context.WithValue(context.Background(), "reqID", "abc-123")
logger.InfoContext(ctx, "fetching user", "user_id", 42)
// Logger with common fields
requestLogger := logger.With(
"request_id", "req-789",
"user_id", 100,
)
requestLogger.Info("query starting")
requestLogger.Info("query completed", "rows", 5)
}
Code Quality Tool Chain
# go vet — official static analysis
go vet ./...
# golangci-lint — integrated linter suite (recommended)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run ./...
# staticcheck — advanced static analysis
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
# govulncheck — security vulnerability scanner
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# gofumpt — stricter version of gofmt
go install mvdan.cc/gofumpt@latest
gofumpt -l -w .
# deadcode — find unused code
go install golang.org/x/tools/cmd/deadcode@latest
deadcode -test ./...
# .golangci.yml
run:
timeout: 5m
linters:
enable:
- errcheck # Check error handling
- gosimple # Simplifiable code
- govet # go vet
- ineffassign # Inefficient assignment
- staticcheck # Static analysis
- unused # Unused code
- gofmt # Format check
- goimports # Import sorting
- misspell # Typo checking
- exhaustive # Switch completeness
linters-settings:
errcheck:
check-type-assertions: true
govet:
enable-all: true
Module and Dependency Management
# Initialize module
go mod init github.com/myorg/myapp
# Add dependency
go get github.com/gin-gonic/gin@v1.9.1
go get golang.org/x/sync@latest
# Remove unused dependencies
go mod tidy
# Vendoring (offline/reproducible builds)
go mod vendor
# Audit dependencies for security
go list -m all | govulncheck -
# View dependency tree
go mod graph | head -20
# Check why a package is needed
go mod why github.com/some/package
# Upgrade dependencies
go get -u ./... # Upgrade all
go get -u=patch ./... # Patch version only
go get github.com/gin-gonic/gin # Upgrade to latest
Production Checklist
Pre-deployment verification checklist:
Build & Tests
✅ go test -race ./... — Detect race conditions
✅ go vet ./... — Static analysis
✅ golangci-lint run ./... — Lint passing
✅ govulncheck ./... — No security vulnerabilities
✅ go test -cover (≥80%) — Test coverage sufficient
Code Quality
✅ Error handling: use errors.Is/As, wrap with fmt.Errorf("%w")
✅ Context propagation: context.Context as first parameter in all API funcs
✅ Resource cleanup: defer Close/Cancel to ensure cleanup
✅ Cancellation: handle ctx.Done() in long-running operations
Configuration & Security
✅ Secrets from environment variables (no hardcoding)
✅ Timeouts configured (HTTP client/server)
✅ Rate limiting applied
✅ Input validation in place
Operations
✅ Structured logging (JSON, slog)
✅ Health check endpoints (/health, /ready)
✅ Graceful shutdown (SIGTERM handling)
✅ Metrics exposed (Prometheus /metrics)
✅ Resource limits set (GOMEMLIMIT)
Key Takeaways
| Area | Recommended Pattern |
|---|---|
| Errors | fmt.Errorf("%w") wrapping, errors.Is/As checking |
| Context | First parameter, propagate timeouts/cancellation |
| Interfaces | Small focused interfaces, consumer-defined |
| Config | Environment variables + defaults, validate at startup |
| Logging | log/slog JSON handler (Go 1.21+) |
| Concurrency | errgroup, worker pools, done channel pattern |
| Quality | golangci-lint, govulncheck, -race testing |
- Go Proverb: "Clear is better than clever" — prefer clear code over clever tricks
- Context Rule: Never store context in structs; always pass as parameter
- Interface Rule: Consumer (caller) defines interfaces, not implementation