# Go (Golang) Best Practices Reference > Comprehensive reference for idiomatic, production-grade Go. Concrete examples throughout. --- ## Table of Contents 1. [Idiomatic Go](#1-idiomatic-go) 2. [Concurrency](#2-concurrency) 3. [Project Structure](#3-project-structure) 4. [Networking](#4-networking) 5. [Testing](#5-testing) 6. [Performance](#6-performance) 7. [Observability](#7-observability) 8. [Build & Deploy](#8-build--deploy) --- ## 1. Idiomatic Go ### Error Handling #### Wrapping errors with `%w` Always wrap errors with context using `fmt.Errorf` and `%w` so callers can unwrap: ```go func readConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("readConfig %s: %w", path, err) } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("readConfig unmarshal: %w", err) } return &cfg, nil } // Caller can inspect: if errors.Is(err, os.ErrNotExist) { // handle missing file } ``` #### Sentinel errors Define package-level sentinel errors for conditions callers need to check: ```go package repo import "errors" var ( ErrNotFound = errors.New("not found") ErrConflict = errors.New("conflict") ErrUnauthorized = errors.New("unauthorized") ) func (r *Repo) GetDevice(id string) (*Device, error) { d, ok := r.devices[id] if !ok { return nil, fmt.Errorf("device %s: %w", id, ErrNotFound) } return d, nil } ``` #### Custom error types Use when you need structured error data beyond a message: ```go type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation: %s — %s", e.Field, e.Message) } // Check with errors.As: var ve *ValidationError if errors.As(err, &ve) { log.Printf("bad field: %s", ve.Field) } ``` #### Don't use `errors.Is` / `errors.As` for every error Only sentinel/typed errors need programmatic inspection. Most errors just bubble up with wrapping — that's fine. Don't over-engineer. ### Interface Design #### Small interfaces The best Go interfaces have 1–3 methods. The stdlib sets the standard: ```go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } ``` Define your own small interfaces close to where they're consumed: ```go // In the handler package, not the repo package type DeviceFinder interface { FindDevice(ctx context.Context, id string) (*model.Device, error) } type handler struct { finder DeviceFinder } ``` #### Accept interfaces, return structs ```go // Good — accepts interface, returns concrete type func NewService(repo DeviceRepo, logger *slog.Logger) *Service { return &Service{repo: repo, logger: logger} } // Bad — returning an interface hides the concrete type unnecessarily func NewService(repo DeviceRepo) ServiceInterface { ... } ``` This lets callers benefit from the full concrete type while keeping your function testable via the interface parameter. #### Don't define interfaces prematurely > "The bigger the interface, the weaker the abstraction." — Rob Pike Define interfaces when you have ≥2 implementations OR need to mock in tests. Not before. ### Embedding Use struct embedding for composition, not inheritance: ```go type BaseModel struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Device struct { BaseModel Name string `json:"name"` IP string `json:"ip"` SNMPPort int `json:"snmp_port"` } // Device now has .ID, .CreatedAt, etc. promoted d := Device{Name: "switch-01"} d.ID = "dev_123" ``` Embed interfaces to signal partial implementation or to compose behaviors: ```go type ReadCloser struct { io.Reader io.Closer } ``` **Watch out**: Embedding promotes all methods, including ones you don't want. If the embedded type has a `String()` method, your type now satisfies `fmt.Stringer` — maybe unintentionally. ### Functional Options Pattern For constructors with many optional parameters: ```go type Server struct { addr string readTimeout time.Duration writeTimeout time.Duration maxConns int logger *slog.Logger } type Option func(*Server) func WithReadTimeout(d time.Duration) Option { return func(s *Server) { s.readTimeout = d } } func WithWriteTimeout(d time.Duration) Option { return func(s *Server) { s.writeTimeout = d } } func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } } func WithLogger(l *slog.Logger) Option { return func(s *Server) { s.logger = l } } func NewServer(addr string, opts ...Option) *Server { s := &Server{ addr: addr, readTimeout: 5 * time.Second, // sensible defaults writeTimeout: 10 * time.Second, maxConns: 100, logger: slog.Default(), } for _, opt := range opts { opt(s) } return s } // Usage: srv := NewServer(":8080", WithReadTimeout(10*time.Second), WithLogger(myLogger), ) ``` This is clean, extensible, and self-documenting. Prefer it over config structs when options are genuinely optional. --- ## 2. Concurrency ### Goroutines + Channels #### Basic patterns ```go // Fire-and-forget (careful — no error handling) go processEvent(event) // Channel for result ch := make(chan Result, 1) go func() { ch <- expensiveComputation() }() result := <-ch ``` #### Always know how a goroutine ends Every goroutine you start should have a clear termination path. Leaked goroutines are memory leaks. ```go // Bad — goroutine leaks if ctx is never cancelled and ch is never read go func() { ch <- doWork(ctx) }() // Good — select with context go func() { select { case ch <- doWork(ctx): case <-ctx.Done(): } }() ``` ### sync.WaitGroup For waiting on a known set of goroutines: ```go func processAll(ctx context.Context, items []Item) error { var wg sync.WaitGroup errs := make(chan error, len(items)) for _, item := range items { wg.Add(1) go func() { defer wg.Done() if err := process(ctx, item); err != nil { errs <- err } }() } wg.Wait() close(errs) for err := range errs { return err // return first error } return nil } ``` ### Mutex vs Channels **Rule of thumb**: - **Mutex**: Protecting shared state (maps, counters, caches) - **Channels**: Communicating between goroutines, coordinating work ```go // Mutex — protecting a shared map type SafeMap struct { mu sync.RWMutex m map[string]int } func (s *SafeMap) Get(key string) (int, bool) { s.mu.RLock() defer s.mu.RUnlock() v, ok := s.m[key] return v, ok } func (s *SafeMap) Set(key string, val int) { s.mu.Lock() defer s.mu.Unlock() s.m[key] = val } // Channel — pipeline stage 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 } ``` Use `sync.RWMutex` when reads vastly outnumber writes. ### context.Context #### Cancellation ```go func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() go worker(ctx) // Cancel after signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh cancel() // all workers using ctx will see Done() } func worker(ctx context.Context) { for { select { case <-ctx.Done(): log.Println("worker shutting down:", ctx.Err()) return default: doWork() } } } ``` #### Timeouts ```go ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // always defer cancel to release resources result, err := fetchFromAPI(ctx, url) if errors.Is(err, context.DeadlineExceeded) { log.Println("API call timed out") } ``` #### Context values — use sparingly Only for request-scoped data that crosses API boundaries (trace IDs, auth tokens). Never for optional parameters. ```go type ctxKey string const requestIDKey ctxKey = "request_id" func WithRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey, id) } func RequestID(ctx context.Context) string { id, _ := ctx.Value(requestIDKey).(string) return id } ``` ### errgroup `golang.org/x/sync/errgroup` — the standard tool for concurrent work with error propagation: ```go import "golang.org/x/sync/errgroup" func fetchAll(ctx context.Context, urls []string) ([]Response, error) { g, ctx := errgroup.WithContext(ctx) responses := make([]Response, len(urls)) for i, url := range urls { g.Go(func() error { resp, err := fetch(ctx, url) if err != nil { return fmt.Errorf("fetch %s: %w", url, err) } responses[i] = resp // safe — each goroutine writes to unique index return nil }) } if err := g.Wait(); err != nil { return nil, err } return responses, nil } ``` With concurrency limit: ```go g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) // max 10 concurrent goroutines ``` ### Worker Pool ```go func workerPool(ctx context.Context, jobs <-chan Job, numWorkers int) <-chan Result { results := make(chan Result, numWorkers) var wg sync.WaitGroup for range numWorkers { wg.Add(1) go func() { defer wg.Done() for job := range jobs { select { case <-ctx.Done(): return case results <- process(job): } } }() } go func() { wg.Wait() close(results) }() return results } ``` ### Fan-Out / Fan-In ```go // Fan-out: distribute work to multiple goroutines func fanOut(ctx context.Context, input <-chan Task, workers int) []<-chan Result { channels := make([]<-chan Result, workers) for i := range workers { channels[i] = worker(ctx, input) } return channels } // Fan-in: merge multiple channels into one func fanIn(ctx context.Context, channels ...<-chan Result) <-chan Result { merged := make(chan Result) var wg sync.WaitGroup for _, ch := range channels { wg.Add(1) go func() { defer wg.Done() for result := range ch { select { case merged <- result: case <-ctx.Done(): return } } }() } go func() { wg.Wait() close(merged) }() return merged } ``` --- ## 3. Project Structure ### Standard Layout ``` myproject/ ├── cmd/ │ ├── server/ # main application entry point │ │ └── main.go │ └── cli/ # CLI tool │ └── main.go ├── internal/ # private packages — can't be imported by other modules │ ├── server/ # HTTP/gRPC server setup │ ├── handler/ # request handlers │ ├── service/ # business logic │ ├── repo/ # data access │ ├── model/ # domain types │ └── config/ # configuration ├── proto/ # protobuf definitions │ └── v1/ ├── migrations/ # database migrations ├── docs/ ├── scripts/ ├── Makefile ├── Dockerfile ├── go.mod └── go.sum ``` #### The `/pkg` debate **Skip `/pkg` for most projects.** It was popularized by early Go projects but adds no real value — `internal/` enforces visibility; `pkg/` is just convention. If you need to export packages for other modules, put them at the root or in a clearly named directory. #### `internal/` is your friend Anything in `internal/` can't be imported by other Go modules. Use it aggressively — you can always promote later. ### Dependency Injection with Wire [Wire](https://github.com/google/wire) generates DI code at compile time — no runtime reflection. ```go // internal/server/wire.go //go:build wireinject package server import "github.com/google/wire" func InitializeApp(cfg *config.Config) (*App, error) { wire.Build( NewApp, handler.NewDeviceHandler, service.NewDeviceService, repo.NewPostgresRepo, db.NewPool, ) return nil, nil } ``` Run `wire ./internal/server/` to generate the actual initialization code. **Alternative**: For simpler projects, manual DI in `main()` is fine. Don't use Wire until you have enough components to justify it. ### Config Management #### `envconfig` — simple, env-var-based ```go import "github.com/kelseyhightower/envconfig" type Config struct { Port int `envconfig:"PORT" default:"8080"` DatabaseURL string `envconfig:"DATABASE_URL" required:"true"` LogLevel string `envconfig:"LOG_LEVEL" default:"info"` Timeout time.Duration `envconfig:"TIMEOUT" default:"30s"` } func LoadConfig() (*Config, error) { var cfg Config if err := envconfig.Process("", &cfg); err != nil { return nil, fmt.Errorf("load config: %w", err) } return &cfg, nil } ``` #### Alternatives | Library | Best for | |---------|----------| | `envconfig` | Simple env-var config, 12-factor apps | | `koanf` | Multi-source config (env, files, flags), lighter than Viper | | `viper` | Kitchen-sink config (heavy, pulls in many deps) | | `ff` | Flags-first config with env var fallback | **Recommendation**: Start with `envconfig`. Graduate to `koanf` if you need config files or multiple sources. Avoid Viper unless you're already using it. ### Makefile Patterns ```makefile .PHONY: build test lint run clean proto generate # Variables BINARY := myapp GO := go GOFLAGS := -trimpath LDFLAGS := -s -w -X main.version=$(shell git describe --tags --always --dirty) CGO_ENABLED := 0 ## Build build: $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/server ## Run locally run: $(GO) run ./cmd/server ## Test test: $(GO) test -race -count=1 ./... test-integration: $(GO) test -race -count=1 -tags=integration -timeout=5m ./... test-coverage: $(GO) test -race -coverprofile=coverage.out ./... $(GO) tool cover -html=coverage.out -o coverage.html ## Lint lint: golangci-lint run ./... ## Proto generation proto: buf generate ## Clean clean: rm -rf bin/ coverage.out coverage.html ## Generate (wire, mocks, etc.) generate: $(GO) generate ./... ## All — CI pipeline all: lint test build ``` --- ## 4. Networking ### net/http Server Best Practices #### Always set timeouts ```go srv := &http.Server{ Addr: ":8080", Handler: router, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, // For request body size limits: MaxHeaderBytes: 1 << 20, // 1 MB } ``` Never use `http.ListenAndServe(":8080", nil)` in production — no timeouts = DDoS target. #### Graceful shutdown ```go func run(ctx context.Context) error { srv := &http.Server{ Addr: ":8080", Handler: newRouter(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } // Start server in goroutine errCh := make(chan error, 1) go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { errCh <- err } }() // Wait for interrupt select { case err := <-errCh: return fmt.Errorf("server error: %w", err) case <-ctx.Done(): } // Graceful shutdown with timeout shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("shutdown: %w", err) } return nil } ``` #### Middleware chains ```go // Middleware type type Middleware func(http.Handler) http.Handler // Chain composes middleware func Chain(h http.Handler, mw ...Middleware) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } // Logging middleware func LoggingMiddleware(logger *slog.Logger) Middleware { return func(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) logger.Info("request", "method", r.Method, "path", r.URL.Path, "status", wrapped.statusCode, "duration", time.Since(start), ) }) } } // Recovery middleware func RecoveryMiddleware(logger *slog.Logger) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { logger.Error("panic recovered", "error", err, "stack", string(debug.Stack()), ) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } } // Usage handler := Chain(router, RecoveryMiddleware(logger), LoggingMiddleware(logger), CORSMiddleware(), ) ``` #### responseWriter wrapper for capturing status ```go type responseWriter struct { http.ResponseWriter statusCode int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } ``` ### gRPC + Protobuf #### Proto file conventions ```protobuf syntax = "proto3"; package towerops.device.v1; option go_package = "github.com/towerops/towerops/gen/device/v1;devicev1"; service DeviceService { rpc GetDevice(GetDeviceRequest) returns (GetDeviceResponse); rpc ListDevices(ListDevicesRequest) returns (ListDevicesResponse); rpc WatchDevices(WatchDevicesRequest) returns (stream DeviceEvent); } message GetDeviceRequest { string id = 1; } message GetDeviceResponse { Device device = 1; } ``` #### Code generation with buf ```yaml # buf.gen.yaml version: v2 plugins: - remote: buf.build/protocolbuffers/go out: gen opt: paths=source_relative - remote: buf.build/grpc/go out: gen opt: paths=source_relative ``` #### gRPC interceptors (middleware equivalent) ```go import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Unary interceptor for logging func loggingInterceptor(logger *slog.Logger) grpc.UnaryServerInterceptor { return func( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { start := time.Now() resp, err := handler(ctx, req) logger.Info("grpc", "method", info.FullMethod, "duration", time.Since(start), "error", err, ) return resp, err } } // Recovery interceptor func recoveryInterceptor(logger *slog.Logger) grpc.UnaryServerInterceptor { return func( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (resp any, err error) { defer func() { if r := recover(); r != nil { logger.Error("grpc panic", "error", r, "stack", string(debug.Stack())) err = status.Errorf(codes.Internal, "internal error") } }() return handler(ctx, req) } } // Server setup srv := grpc.NewServer( grpc.ChainUnaryInterceptor( recoveryInterceptor(logger), loggingInterceptor(logger), ), grpc.ChainStreamInterceptor( // stream interceptors... ), ) ``` #### Server-side streaming ```go func (s *server) WatchDevices( req *pb.WatchDevicesRequest, stream pb.DeviceService_WatchDevicesServer, ) error { ctx := stream.Context() ch := s.deviceEvents.Subscribe() defer s.deviceEvents.Unsubscribe(ch) for { select { case <-ctx.Done(): return ctx.Err() case event := <-ch: if err := stream.Send(event); err != nil { return fmt.Errorf("send event: %w", err) } } } } ``` ### HTTP Client Patterns #### Reuse clients — never create per-request ```go // Package-level or injected client with connection pooling var httpClient = &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, }, } ``` #### Retry with exponential backoff ```go func doWithRetry(ctx context.Context, req *http.Request, maxRetries int) (*http.Response, error) { var resp *http.Response var err error backoff := 100 * time.Millisecond for attempt := range maxRetries { resp, err = httpClient.Do(req.Clone(ctx)) if err != nil { // Network error — retry } else if resp.StatusCode < 500 && resp.StatusCode != 429 { return resp, nil } else { resp.Body.Close() // must close before retry } if attempt == maxRetries-1 { break } select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(backoff): } backoff *= 2 // exponential } if err != nil { return nil, fmt.Errorf("after %d retries: %w", maxRetries, err) } return resp, nil } ``` #### Always close response bodies ```go resp, err := httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() // Drain body even if you don't need it — allows connection reuse io.Copy(io.Discard, resp.Body) ``` --- ## 5. Testing ### Table-Driven Tests The Go standard for test organization: ```go func TestParsePort(t *testing.T) { tests := []struct { name string input string want int wantErr bool }{ {name: "valid port", input: "8080", want: 8080}, {name: "zero", input: "0", want: 0}, {name: "max", input: "65535", want: 65535}, {name: "negative", input: "-1", wantErr: true}, {name: "too high", input: "65536", wantErr: true}, {name: "not a number", input: "abc", wantErr: true}, {name: "empty", input: "", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParsePort(tt.input) if tt.wantErr { if err == nil { t.Fatal("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if got != tt.want { t.Errorf("got %d, want %d", got, tt.want) } }) } } ``` ### testify vs stdlib **stdlib** is fine for most cases. `t.Errorf` / `t.Fatalf` with clear messages work great. **testify** helps when assertions get complex: ```go import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDeviceService(t *testing.T) { svc := NewDeviceService(mockRepo) device, err := svc.Get(ctx, "dev_123") require.NoError(t, err) // stops test on failure assert.Equal(t, "switch-01", device.Name) assert.NotEmpty(t, device.ID) assert.WithinDuration(t, time.Now(), device.CreatedAt, time.Minute) } ``` `require` = fatal on failure (test stops). `assert` = log and continue. Use `require` for setup/preconditions, `assert` for actual checks. ### httptest Test HTTP handlers without a real server: ```go func TestGetDevice(t *testing.T) { // Setup repo := &mockRepo{ devices: map[string]*Device{ "dev_1": {ID: "dev_1", Name: "switch-01"}, }, } handler := NewDeviceHandler(repo) // Create request req := httptest.NewRequest(http.MethodGet, "/devices/dev_1", nil) rec := httptest.NewRecorder() // Serve handler.ServeHTTP(rec, req) // Assert assert.Equal(t, http.StatusOK, rec.Code) var got Device err := json.NewDecoder(rec.Body).Decode(&got) require.NoError(t, err) assert.Equal(t, "switch-01", got.Name) } ``` For full integration tests with routing: ```go func TestAPI(t *testing.T) { srv := httptest.NewServer(newRouter()) defer srv.Close() resp, err := http.Get(srv.URL + "/health") require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) } ``` ### Mocking with Interfaces (not frameworks) Define the interface where it's consumed, implement a mock manually: ```go // In handler package type DeviceRepo interface { Get(ctx context.Context, id string) (*model.Device, error) List(ctx context.Context) ([]*model.Device, error) } // In handler test file type mockRepo struct { devices map[string]*model.Device err error } func (m *mockRepo) Get(ctx context.Context, id string) (*model.Device, error) { if m.err != nil { return nil, m.err } d, ok := m.devices[id] if !ok { return nil, repo.ErrNotFound } return d, nil } func (m *mockRepo) List(ctx context.Context) ([]*model.Device, error) { if m.err != nil { return nil, m.err } var result []*model.Device for _, d := range m.devices { result = append(result, d) } return result, nil } ``` This is simpler, more readable, and more maintainable than mock frameworks like gomock. For large interfaces, consider `moq` or `mockery` for code generation — but keep mocks simple. ### Integration Tests with testcontainers-go ```go //go:build integration package repo_test import ( "context" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) func TestPostgresRepo(t *testing.T) { ctx := context.Background() pgContainer, err := postgres.Run(ctx, "postgres:16-alpine", postgres.WithDatabase("testdb"), postgres.WithUsername("test"), postgres.WithPassword("test"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2), ), ) require.NoError(t, err) t.Cleanup(func() { pgContainer.Terminate(ctx) }) connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err) // Run migrations runMigrations(t, connStr) // Create repo with real DB r, err := repo.NewPostgresRepo(ctx, connStr) require.NoError(t, err) // Test actual queries t.Run("create and get", func(t *testing.T) { device := &model.Device{Name: "test-switch"} err := r.Create(ctx, device) require.NoError(t, err) assert.NotEmpty(t, device.ID) got, err := r.Get(ctx, device.ID) require.NoError(t, err) assert.Equal(t, "test-switch", got.Name) }) } ``` ### go test Flags ```bash # Race detector — always use in CI go test -race ./... # Disable test caching (important for flaky test detection) go test -count=1 ./... # Run tests in parallel (default is GOMAXPROCS) go test -parallel=4 ./... # Only run matching tests go test -run TestGetDevice ./internal/handler/ # Verbose output go test -v ./... # Short mode (skip slow tests) go test -short ./... # Timeout go test -timeout=5m ./... # Integration tests only (with build tag) go test -tags=integration ./... # Combined for CI: go test -race -count=1 -timeout=10m ./... ``` In test code, respect `-short`: ```go func TestSlowIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") } // slow test... } ``` --- ## 6. Performance ### pprof Profiling #### Enable in HTTP server ```go import _ "net/http/pprof" // If using default mux, pprof is auto-registered. // For custom mux, register explicitly: mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/profile", pprof.Profile) mux.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP) mux.HandleFunc("/debug/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP) ``` #### Capture and analyze ```bash # CPU profile (30 seconds) go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30 # Heap profile go tool pprof http://localhost:8080/debug/pprof/heap # Goroutine dump curl http://localhost:8080/debug/pprof/goroutine?debug=2 # Interactive commands in pprof: # top20 — top 20 functions by CPU/memory # list funcName — show annotated source # web — open flamegraph in browser ``` ### Benchmarking ```go func BenchmarkProcessEvent(b *testing.B) { event := createTestEvent() b.ResetTimer() for b.Loop() { processEvent(event) } } // With sub-benchmarks for different sizes func BenchmarkParse(b *testing.B) { sizes := []int{10, 100, 1000, 10000} for _, size := range sizes { b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { data := generateData(size) b.ResetTimer() for b.Loop() { parse(data) } }) } } // Report allocations func BenchmarkBuildString(b *testing.B) { b.ReportAllocs() for b.Loop() { buildString(1000) } } ``` ```bash # Run benchmarks go test -bench=. -benchmem ./... # Compare benchmarks (install benchstat) go test -bench=. -count=10 > old.txt # ... make changes ... go test -bench=. -count=10 > new.txt benchstat old.txt new.txt ``` ### Memory Allocation Patterns #### sync.Pool — reuse temporary objects ```go var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } func processRequest(data []byte) string { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() buf.Write(data) // ... process ... return buf.String() } ``` #### Pre-allocation ```go // Bad — grows the slice repeatedly var result []string for _, item := range items { result = append(result, item.Name) } // Good — allocate once result := make([]string, 0, len(items)) for _, item := range items { result = append(result, item.Name) } // Same for maps m := make(map[string]int, len(items)) ``` ### Escape Analysis Check what escapes to the heap: ```bash go build -gcflags='-m -m' ./... 2>&1 | grep 'escapes to heap' ``` Tips to reduce heap allocations: - Return values instead of pointers when the struct is small - Use value receivers for small types - Avoid closures that capture variables (they often escape) - Pre-size slices and maps ```go // This might stay on stack (small struct, returned by value) func newPoint(x, y int) Point { return Point{X: x, Y: y} } // This always escapes to heap (pointer returned) func newPoint(x, y int) *Point { return &Point{X: x, Y: y} } ``` ### String Building ```go // Bad — O(n²) concatenation s := "" for _, item := range items { s += item.Name + "," } // Good — strings.Builder var b strings.Builder b.Grow(len(items) * 20) // estimate capacity for i, item := range items { if i > 0 { b.WriteByte(',') } b.WriteString(item.Name) } result := b.String() // For simple joins result := strings.Join(names, ",") ``` --- ## 7. Observability ### Structured Logging with slog (Go 1.21+) ```go import "log/slog" // Setup func setupLogger(level string) *slog.Logger { var lvl slog.Level switch level { case "debug": lvl = slog.LevelDebug case "warn": lvl = slog.LevelWarn case "error": lvl = slog.LevelError default: lvl = slog.LevelInfo } return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: lvl, AddSource: true, // adds file:line to log entries })) } // Usage logger := setupLogger("info") logger.Info("server started", "addr", ":8080", "version", version) logger.Error("query failed", "error", err, "query", query, "duration", time.Since(start), ) // With context (carries request ID, trace ID, etc.) logger.InfoContext(ctx, "device updated", "device_id", device.ID, "changes", changes, ) // Child logger with common fields reqLogger := logger.With( "request_id", requestID, "method", r.Method, "path", r.URL.Path, ) ``` #### slog groups for structured fields ```go logger.Info("request", slog.Group("http", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", status), ), slog.Group("timing", slog.Duration("total", totalDuration), slog.Duration("db", dbDuration), ), ) // Output: {"http":{"method":"GET","path":"/devices","status":200},"timing":{"total":"12ms","db":"3ms"}} ``` ### OpenTelemetry #### Tracing setup ```go import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) func initTracer(ctx context.Context, serviceName, version string) (func(), error) { exporter, err := otlptracegrpc.New(ctx) if err != nil { return nil, fmt.Errorf("create exporter: %w", err) } res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceNameKey.String(serviceName), semconv.ServiceVersionKey.String(version), ), ) if err != nil { return nil, fmt.Errorf("create resource: %w", err) } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.AlwaysSample()), // tune in prod ) otel.SetTracerProvider(tp) cleanup := func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() tp.Shutdown(ctx) } return cleanup, nil } ``` #### Using traces ```go var tracer = otel.Tracer("myapp/service") func (s *DeviceService) GetDevice(ctx context.Context, id string) (*Device, error) { ctx, span := tracer.Start(ctx, "DeviceService.GetDevice", trace.WithAttributes(attribute.String("device.id", id)), ) defer span.End() device, err := s.repo.Get(ctx, id) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return nil, err } return device, nil } ``` ### Prometheus Metrics ```go import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( httpRequestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total HTTP requests", }, []string{"method", "path", "status"}, ) httpRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "HTTP request duration in seconds", Buckets: prometheus.DefBuckets, }, []string{"method", "path"}, ) activeConnections = promauto.NewGauge( prometheus.GaugeOpts{ Name: "active_connections", Help: "Number of active connections", }, ) ) // Metrics 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() httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(wrapped.statusCode)).Inc() httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration) }) } // Expose metrics endpoint mux.Handle("/metrics", promhttp.Handler()) ``` ### Health Checks ```go type HealthChecker struct { db *sql.DB redis *redis.Client } type HealthStatus struct { Status string `json:"status"` Checks map[string]string `json:"checks"` } // Liveness — is the process alive? Keep it simple. func (h *HealthChecker) Liveness(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) } // Readiness — can the process handle traffic? func (h *HealthChecker) Readiness(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() status := HealthStatus{ Status: "ok", Checks: make(map[string]string), } if err := h.db.PingContext(ctx); err != nil { status.Status = "degraded" status.Checks["database"] = err.Error() } else { status.Checks["database"] = "ok" } if err := h.redis.Ping(ctx).Err(); err != nil { status.Status = "degraded" status.Checks["redis"] = err.Error() } else { status.Checks["redis"] = "ok" } code := http.StatusOK if status.Status != "ok" { code = http.StatusServiceUnavailable } w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) json.NewEncoder(w).Encode(status) } // Register mux.HandleFunc("GET /healthz", health.Liveness) mux.HandleFunc("GET /readyz", health.Readiness) ``` --- ## 8. Build & Deploy ### Multi-Stage Docker Builds ```dockerfile # Stage 1: Build FROM golang:1.24-alpine AS builder WORKDIR /app # Cache dependencies COPY go.mod go.sum ./ RUN go mod download # Copy source and build COPY . . RUN CGO_ENABLED=0 GOOS=linux go build \ -trimpath \ -ldflags="-s -w -X main.version=$(cat VERSION)" \ -o /app/server \ ./cmd/server # Stage 2: Runtime FROM gcr.io/distroless/static-debian12:nonroot COPY --from=builder /app/server /server EXPOSE 8080 USER nonroot:nonroot ENTRYPOINT ["/server"] ``` **Key points**: - `CGO_ENABLED=0` for static binary (no libc dependency) - `-trimpath` removes local paths from binary - `-ldflags="-s -w"` strips debug info (smaller binary) - `distroless` or `scratch` for minimal attack surface - `nonroot` user for security ### CGO Considerations **Default: Avoid CGO.** `CGO_ENABLED=0` gives you a static binary that runs anywhere. When you need CGO (SQLite, certain crypto libs): ```dockerfile FROM golang:1.24-alpine AS builder RUN apk add --no-cache gcc musl-dev # CGO_ENABLED=1 is default when gcc is available RUN go build -o /app/server ./cmd/server FROM alpine:3.20 RUN apk add --no-cache ca-certificates COPY --from=builder /app/server /server ENTRYPOINT ["/server"] ``` ### Cross-Compilation ```bash # Build for Linux AMD64 GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 ./cmd/server # Build for Linux ARM64 GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 ./cmd/server # Build for macOS ARM64 (Apple Silicon) GOOS=darwin GOARCH=arm64 go build -o myapp-darwin-arm64 ./cmd/server # Build for Windows GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe ./cmd/server ``` Cross-compilation only works cleanly with `CGO_ENABLED=0`. If you need CGO + cross-compile, use Docker or [zig as CC](https://dev.to/kristoff/zig-makes-go-cross-compilation-just-work-29ho). ### GoReleaser ```yaml # .goreleaser.yaml version: 2 builds: - id: server main: ./cmd/server binary: myapp env: - CGO_ENABLED=0 goos: - linux - darwin goarch: - amd64 - arm64 ldflags: - -s -w - -X main.version={{.Version}} - -X main.commit={{.Commit}} - -X main.date={{.Date}} archives: - format: tar.gz name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" dockers: - image_templates: - "ghcr.io/myorg/myapp:{{ .Version }}-amd64" dockerfile: Dockerfile use: buildx build_flag_templates: - "--platform=linux/amd64" changelog: sort: asc filters: exclude: - "^docs:" - "^test:" - "^ci:" ``` ```bash # Test locally goreleaser release --snapshot --clean # Release (triggered by git tag) git tag v1.0.0 git push origin v1.0.0 # CI runs: goreleaser release ``` ### Linting with golangci-lint ```yaml # .golangci.yml run: timeout: 5m linters: enable: - errcheck # unchecked errors - govet # vet checks - staticcheck # comprehensive static analysis - unused # unused code - gosimple # simplifications - ineffassign # useless assignments - typecheck # type checking - gocritic # opinionated checks - revive # flexible linter (replaces golint) - misspell # spelling - prealloc # pre-allocation suggestions - unconvert # unnecessary conversions - noctx # HTTP requests without context - bodyclose # unclosed HTTP response bodies - errname # error naming conventions - errorlint # error wrapping checks - exhaustive # exhaustive enum switches - copyloopvar # loop variable capture bugs (Go <1.22) linters-settings: gocritic: enabled-tags: - diagnostic - performance disabled-checks: - ifElseChain revive: rules: - name: exported arguments: [checkPrivateReceivers] - name: blank-imports - name: context-as-argument - name: error-return - name: error-naming - name: increment-decrement - name: var-naming govet: enable-all: true issues: exclude-rules: - path: _test\.go linters: - errcheck - gocritic max-issues-per-linter: 0 max-same-issues: 0 ``` ```bash # Run golangci-lint run ./... # Fix auto-fixable issues golangci-lint run --fix ./... # Only new issues (great for CI on PRs) golangci-lint run --new-from-rev=main ./... ``` --- ## Quick Reference: Go Proverbs - Don't communicate by sharing memory; share memory by communicating. - Concurrency is not parallelism. - The bigger the interface, the weaker the abstraction. - Make the zero value useful. - `interface{}` says nothing (use generics or concrete types). - A little copying is better than a little dependency. - Clear is better than clever. - Errors are values — program with them. - Don't just check errors, handle them gracefully. ## Further Reading - [Effective Go](https://go.dev/doc/effective_go) - [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) - [Go Proverbs](https://go-proverbs.github.io/) - [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) - [100 Go Mistakes](https://100go.co/) - [Go Blog](https://go.dev/blog/)