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 pproffor 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
validatorlib
Related Notes
- 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