Starphix
Software Engineering / Sources
The Lab / Sources / Software Engineering / Languages/Go.md

Go

Idiomatic Go patterns, concurrency, and modern tooling for maintainable Go systems.


Go Philosophy

"A little copying is better than a little dependency." — Rob Pike "Clear is better than clever."

Core Principles

  • Simplicity — Readable code > clever abstractions
  • Explicit errors — Return errors as values; handle at call site
  • Composition — Embed interfaces; prefer small interfaces
  • Concurrency — Goroutines + channels; don't communicate by sharing memory
  • Standard library first — Avoid frameworks until needed

Modern Go (1.22+)

Error Handling (1.13+)

// Wrap with context
if err != nil {
    return fmt.Errorf("find user %s: %w", id, err)
}

// Check with errors.Is / errors.As
if errors.Is(err, sql.ErrNoRows) {
    return ErrNotFound
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
    return ErrDuplicateKey
}

// Custom error types
type ErrNotFound struct{ ID string }
func (e ErrNotFound) Error() string { return "not found: " + e.ID }

Generics (1.18+)

// Generic Result type
type Result[T any] struct {
    Value T
    Err   error
}

func Ok[T any](v T) Result[T] { return Result[T]{Value: v} }
func Err[T any](err error) Result[T] { return Result[T]{Err: err} }

func (r Result[T]) Unwrap() (T, error) { return r.Value, r.Err }

// Generic repository interface
type Repository[T any, ID comparable] interface {
    FindByID(ctx context.Context, id ID) (T, error)
    Save(ctx context.Context, entity T) error
}

Structured Logging (slog, 1.21+)

import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
    AddSource: true,
}))
slog.SetDefault(logger)

slog.Info("user created", "user_id", id, "email", email)
slog.Error("failed to save", "err", err, "user_id", id)

Project Structure (Clean Architecture)

.
├── cmd/
│   └── api/
│       └── main.go           # Composition root
├── internal/
│   ├── domain/
│   │   ├── entity/
│   │   ├── valueobject/
│   │   ├── service/
│   │   ├── event/
│   │   └── repository/       # Interfaces
│   ├── application/
│   │   ├── command/
│   │   ├── query/
│   │   └── dto/
│   ├── infrastructure/
│   │   ├── persistence/      # SQL, Redis implementations
│   │   ├── http/             # Chi, Gin, Echo routers
│   │   ├── messaging/        # Kafka, NATS, RabbitMQ
│   │   └── config/           # Viper, envconfig
│   └── platform/             # Cross-cutting: logging, metrics, tracing
├── pkg/                      # Public reusable packages
├── go.mod
├── go.sum
└── Makefile

Tooling Stack (2024+)

Category Recommended Alternatives
Dependency Mgmt Go modules (go mod) —
Build go build / go generate Mage, Task
Linting golangci-lint (aggregator) staticcheck, govet
Formatting gofmt / gofumpt —
Testing go test + testify / ginkgo stretchr/testify
Mock Generation mockery / gomock —
API Generation oapi-codegen (OpenAPI) buf (Protobuf/gRPC)
Database sqlc (type-safe SQL) GORM, ent, bun
Migrations golang-migrate / atlas sqlc + goose
Observability OpenTelemetry Go Prometheus client
Vulnerability Scan govulncheck Trivy, Snyk

golangci-lint Config (.golangci.yml)

linters:
  enable:
    - errcheck
    - gosec
    - govet
    - ineffassign
    - staticcheck
    - unused
    - misspell
    - nakedret
    - prealloc
    - bodyclose
    - noctx
    - rowserrcheck
    - sqlclosecheck
    - errorlint
linters-settings:
  errcheck:
    check-type-assertions: true
    check-blank: true
  gosec:
    excludes:
      - G104  # Errors unhandled (we handle explicitly)
issues:
  exclude-rules:
    - path: _test\.go
      linters: [errcheck, gosec]

Database Access

sqlc (Type-Safe SQL)

-- sqlc: UserRepository
-- name: GetUser :one
SELECT id, email, created_at FROM users WHERE id = $1;

-- name: CreateUser :one
INSERT INTO users (id, email, created_at) VALUES ($1, $2, $3)
RETURNING id, email, created_at;
// Generated code — type-safe!
func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
    row := q.db.QueryRowContext(ctx, getUser, id)
    var u User
    err := row.Scan(&u.ID, &u.Email, &u.CreatedAt)
    return u, err
}

Repository Pattern

// domain/repository/user.go
type UserRepository interface {
    FindByID(ctx context.Context, id UserID) (User, error)
    Save(ctx context.Context, u User) error
}

// infrastructure/persistence/postgres_user_repo.go
type PostgresUserRepo struct { q *Queries }

func (r *PostgresUserRepo) FindByID(ctx context.Context, id UserID) (User, error) {
    u, err := r.q.GetUser(ctx, id.String())
    return toDomain(u), err
}

Concurrency Patterns

Worker Pool

func ProcessItems(ctx context.Context, items []Item, workers int) error {
    type job struct { item Item }
    type result struct { err error }

    jobs := make(chan job, len(items))
    results := make(chan result, len(items))

    // Start workers
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := range jobs {
                results <- result{err: processItem(ctx, j.item)}
            }
        }()
    }

    // Send jobs
    go func() {
        for _, item := range items { jobs <- job{item: item} }
        close(jobs)
    }()

    // Wait and collect
    go func() { wg.Wait(); close(results) }()

    for r := range results {
        if r.err != nil { return r.err } // or collect all
    }
    return nil
}

ErrGroup (Structured Concurrency)

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

func ProcessAll(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // concurrency limit

    for _, item := range items {
        item := item // capture loop var
        g.Go(func() error { return processItem(ctx, item) })
    }
    return g.Wait()
}

Context Propagation

// Always pass context as first parameter
func (s *UserService) GetUser(ctx context.Context, id UserID) (User, error) {
    // Timeout at call site
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    return s.repo.FindByID(ctx, id)
}

Channels for Communication

// Pipeline pattern
func generate(ctx context.Context) <-chan Item {
    out := make(chan Item)
    go func() {
        defer close(out)
        for i := 0; ; i++ {
            select {
            case <-ctx.Done(): return
            case out <- Item{ID: i}:
            }
        }
    }()
    return out
}

func process(ctx context.Context, in <-chan Item) <-chan Result {
    out := make(chan Result)
    go func() {
        defer close(out)
        for item := range in {
            select {
            case <-ctx.Done(): return
            case out <- doWork(item):
            }
        }
    }()
    return out
}

HTTP/API Layer

Chi Router (Lightweight, Composable)

r := chi.NewRouter()
r.Use(middleware.RequestID, middleware.RealIP, middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))

r.Route("/api/v1", func(r chi.Router) {
    r.Get("/users/{id}", getUserHandler)
    r.Post("/users", createUserHandler)
})

// Handler with dependency injection
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    user, err := app.UserService.GetUser(r.Context(), UserID(id))
    if err != nil {
        renderError(w, err)
        return
    }
    render.JSON(w, r, user)
}

gRPC + Protobuf (buf)

// proto/user/v1/user.proto
service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
}
buf generate  # generates Go + gRPC + HTTP gateway code

Testing

Unit Tests (table-driven)

func TestUserService_GetUser(t *testing.T) {
    tests := []struct{
        name    string
        setup   func() (UserRepository, func())
        id      UserID
        want    User
        wantErr error
    }{
        {"found", func() (UserRepository, func()) {
            repo := &mockRepo{user: User{ID: "1", Email: "a@b.com"}}
            return repo, func() {}
        }, UserID("1"), User{ID: "1", Email: "a@b.com"}, nil},
        {"not found", func() (UserRepository, func()) {
            return &mockRepo{err: ErrNotFound}, func() {}
        }, UserID("999"), User{}, ErrNotFound},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            repo, cleanup := tc.setup()
            defer cleanup()
            svc := NewUserService(repo)
            got, err := svc.GetUser(context.Background(), tc.id)
            if diff := cmp.Diff(tc.wantErr, err); diff != "" {
                t.Errorf("error mismatch (-want +got):\n%s", diff)
            }
            if diff := cmp.Diff(tc.want, got); diff != "" {
                t.Errorf("user mismatch (-want +got):\n%s", diff)
            }
        })
    }
}

Integration Tests (testcontainers)

func TestPostgresUserRepo_Integration(t *testing.T) {
    ctx := context.Background()
    pgContainer, _ := postgres.RunContainer(ctx, postgres.WithDatabase("test"))
    defer pgContainer.Terminate(ctx)

    connStr, _ := pgContainer.ConnectionString(ctx)
    db, _ := sql.Open("pgx", connStr)
    defer db.Close()

    repo := NewPostgresUserRepo(db)
    // ... test with real DB
}

Contract Tests (Pact Go)

// Consumer test
pact.AddInteraction().
    Given("user exists").
    UponReceiving("request for user").
    WithRequest("GET", "/users/123", nil).
    WillRespondWith(200, map[string]interface{}{
        "id": "123", "email": "test@example.com",
    })

Observability

OpenTelemetry (Standard)

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/trace"
)

func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
    exporter, _ := otlptracegrpc.New(ctx)
    tp := trace.NewTracerProvider(trace.WithBatcher(exporter))
    otel.SetTracerProvider(tp)
    return tp, nil
}

// In handler
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    ctx, span := otel.Tracer("api").Start(r.Context(), "GetUser")
    defer span.End()
    // ...
}

Metrics (Prometheus)

var (
    httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{
        Name: "http_requests_total",
    }, []string{"method", "path", "status"})
)

func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, statusCode: 200}
        next.ServeHTTP(rw, r)
        httpRequests.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(rw.statusCode)).Inc()
    })
}

Performance

  • Escape analysis — Avoid heap allocation; use value receivers, avoid interface{} in hot paths
  • pprof — go tool pprof for CPU, memory, block, mutex profiles
  • Benchmark — go test -bench=. -benchmem
  • Inlining — //go:inline (1.22+); small functions auto-inlined
  • GC tuning — GOGC, GOMEMLIMIT (1.19+); ballast for latency-sensitive services

Security

  • govulncheck in CI — go install golang.org/x/vuln/cmd/govulncheck@latest
  • Gosec — Static analysis for security issues
  • Dependencies — go mod verify; go mod audit (third-party)
  • Secrets — Never in code; env vars + secret manager
  • Input validation — Validate at boundaries; use validator lib

  • Language Agnostic Principles — Universal patterns
  • TypeScript JavaScript — JS/TS patterns
  • Python — Python patterns
  • Rust — Rust patterns
  • Concurrency Patterns — Concurrency deep dive
  • Resilience Patterns — Circuit breaker, retry in Go
  • CI CD Best Practices — Pipeline for Go
  • Architectural Styles — Hexagonal in Go

Sources: Effective Go, Go Blog, Dave Cheney (Practical Go), Go Time Podcast, Uber Go Style Guide, Google Go Style, sqlc, golangci-lint, OpenTelemetry Go, testify, ginkgo, mockery, oapi-codegen, buf, atlas, govulncheck

From the Software Engineering research vault · CC BY 4.0 · all notes · raw markdown