Skip to content

go

Deep Go patterns for project layout, concurrency, error handling, testing, and profiling

seniorv1.0.0senior/skills/go
View source on GitHub

Go

Project Layout

project/
  cmd/
    server/
      main.go
  internal/
    handler/
    service/
    repository/
    model/
  pkg/
    util/
  api/
    openapi.yaml
  migrations/
  test/
  go.mod
  go.sum
  • cmd/ for binary entry points.
  • internal/ for private packages (not importable outside).
  • pkg/ for public reusable libraries.

Concurrency Patterns

Worker Pool

func worker(ctx context.Context, jobs <-chan Job, results chan<- Result) {
    for j := range jobs {
        select {
        case <-ctx.Done():
            return
        case results <- process(j):
        }
    }
}

func runPool(ctx context.Context, jobs []Job, workers int) []Result {
    jobCh := make(chan Job, len(jobs))
    resCh := make(chan Result, len(jobs))

    for range workers {
        go worker(ctx, jobCh, resCh)
    }
    for _, j := range jobs {
        jobCh <- j
    }
    close(jobCh)

    var results []Result
    for range jobs {
        results = append(results, <-resCh)
    }
    return results
}

Fan-Out / Fan-In

func fanOut(ctx context.Context, input <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := range workers {
        ch := make(chan int)
        channels[i] = ch
        go func() {
            defer close(ch)
            for v := range input {
                ch <- v * 2
            }
        }()
    }
    return channels
}

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for v := range ch {
                out <- v
            }
        }()
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Error Handling

Wrapping

import "fmt"

if err != nil {
    return fmt.Errorf("read config: %w", err)
}

Sentinels

var ErrNotFound = errors.New("not found")
var ErrConflict = errors.New("conflict")

if errors.Is(err, ErrNotFound) { }

Custom Types

type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Err }

Interface Design

type Repository interface {
    Get(ctx context.Context, id string) (Entity, error)
    List(ctx context.Context, filter Filter) ([]Entity, error)
    Create(ctx context.Context, e Entity) (Entity, error)
}

// Accept interfaces, return structs.
func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

Testing

Table-Driven

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 1, 2, 3},
        {"negative", -1, -2, -3},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := add(tt.a, tt.b); got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}

httptest

func TestHandler(t *testing.T) {
    srv := httptest.NewServer(handler)
    defer srv.Close()

    resp, err := http.Get(srv.URL + "/health")
    // ...
}

Profiling

go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
go tool pprof -http=:8080 cpu.prof

Build

LDFLAGS = -ldflags "-X main.version=$(VERSION) -w -s"
build:
  CGO_ENABLED=0 go build $(LDFLAGS) -o bin/server ./cmd/server
  • Use -ldflags for version injection.
  • CGO_ENABLED=0 for static binaries.
  • Docker multi-stage with gcr.io/distroless/base.