Golang Patterns

Implement idiomatic Go design patterns and automate boilerplate generation for scalable applications

Golang Patterns is an AI skill that provides idiomatic design patterns and implementation strategies for building Go applications. It covers interface design, error handling, concurrency patterns, dependency injection, and package organization that enable developers to write clean and maintainable Go code.

What Is This?

Overview

Golang Patterns provides structured approaches to solving common Go architecture challenges. It handles designing small, focused interfaces that enable testability and composition, implementing error handling with wrapping and sentinel values for clear failure chains, structuring concurrent workflows using goroutines, channels, and context propagation, applying dependency injection through constructor functions and interface parameters, organizing packages by domain boundaries rather than technical layers, and building middleware chains for HTTP handlers.

Who Should Use This

This skill serves Go developers establishing project architecture, backend teams building microservices in Go, tech leads defining idiomatic patterns for team adoption, and developers transitioning from other languages to Go.

Why Use It?

Problems It Solves

Large interfaces create tight coupling that makes mocking and testing difficult. Unchecked error returns hide failures that surface as obscure bugs in production. Goroutine leaks from unmanaged concurrency consume memory and CPU over time. Package cycles and circular dependencies prevent compilation and indicate poor architectural boundaries.

Core Highlights

Small interfaces with one or two methods enable flexible composition and easy testing. Error wrapping preserves context chains for debugging while supporting sentinel checks. Context propagation manages cancellation and timeouts across goroutine trees. Constructor injection provides dependencies explicitly without global state.

How to Use It?

Basic Usage

package user

import (
    "context"
    "fmt"
)

type User struct {
    ID    string
    Email string
    Name  string
}

type Repository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, u *User) error
}

type Service struct {
    repo Repository
}

func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    u, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("get user %s: %w", id, err)
    }
    return u, nil
}

func (s *Service) Register(ctx context.Context, email, name string) (*User, error) {
    u := &User{ID: generateID(), Email: email, Name: name}
    if err := s.repo.Save(ctx, u); err != nil {
        return nil, fmt.Errorf("register user: %w", err)
    }
    return u, nil
}

Real-World Examples

package worker

import (
    "context"
    "fmt"
    "sync"
)

type Task struct {
    ID   string
    Data string
}

type Processor interface {
    Process(ctx context.Context, t Task) error
}

type Pool struct {
    workers   int
    processor Processor
}

func NewPool(workers int, p Processor) *Pool {
    return &Pool{workers: workers, processor: p}
}

func (p *Pool) Run(ctx context.Context, tasks <-chan Task) error {
    var wg sync.WaitGroup
    errCh := make(chan error, p.workers)

    for i := 0; i < p.workers; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done():
                    return
                case task, ok := <-tasks:
                    if !ok {
                        return
                    }
                    if err := p.processor.Process(ctx, task); err != nil {
                        errCh <- fmt.Errorf("worker %d task %s: %w", id, task.ID, err)
                    }
                }
            }
        }(i)
    }

    wg.Wait()
    close(errCh)

    var errs []error
    for err := range errCh {
        errs = append(errs, err)
    }
    if len(errs) > 0 {
        return fmt.Errorf("%d tasks failed", len(errs))
    }
    return nil
}

Advanced Tips

Define interfaces where they are consumed rather than where they are implemented to keep packages decoupled. Use errgroup from the sync package to manage goroutine lifecycles with combined error handling. Apply the functional options pattern for constructors with many optional parameters.

When to Use It?

Use Cases

Use Golang Patterns when establishing project structure for new Go services, when designing interfaces for testable and composable components, when implementing concurrent worker pools with proper cancellation, or when refactoring tightly coupled Go code.

Related Topics

Go standard library patterns, interface composition, context package usage, channel synchronization, and Go module dependency management complement Go pattern adoption.

Important Notes

Requirements

Go 1.21 or later for current standard library features. Understanding of Go interfaces and goroutine mechanics. Familiarity with the context package for cancellation propagation.

Usage Recommendations

Do: keep interfaces small, ideally one or two methods, for maximum composability. Wrap errors with context using fmt.Errorf and the %w verb for debuggable error chains. Pass context.Context as the first parameter to functions that perform IO or may be cancelled.

Don't: define interfaces with many methods that force implementers to stub unused functions. Ignore returned errors, which hides failures that become difficult to trace. Launch goroutines without a mechanism to signal shutdown and wait for completion.

Limitations

Go lacks generics for some patterns that other languages express concisely with type parameters. Interface-based dependency injection adds indirection that can obscure the call chain. Channel-based concurrency patterns have a learning curve for developers new to Go.