go-best-practices

eous's avatarfrom eous

Go development best practices, patterns, and conventions. Use when writing Go code, reviewing .go files, discussing goroutines, channels, error handling, or Go project structure. Triggers on mentions of Go, Golang, goroutines, channels, defer, interfaces, go mod.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 9, 2025⬇️1downloads

When & Why to Use This Skill

This Claude skill serves as a comprehensive guide for Go (Golang) development, providing expert-level insights into idiomatic coding patterns, project organization, and robust error handling. It enables developers to implement efficient concurrency using goroutines and channels while adhering to industry-standard naming conventions and avoiding common anti-patterns to ensure high-quality, maintainable codebases.

Use Cases

  • Project Initialization: Setting up a standardized Go project structure (cmd/, internal/, pkg/) to ensure scalability and maintainability.
  • Error Management: Implementing advanced error handling techniques, including error wrapping with context and using sentinel errors for better observability.
  • Concurrency Design: Orchestrating complex asynchronous tasks using WaitGroups, buffered channels, and context-based cancellation to prevent goroutine leaks.
  • Interface Abstraction: Designing clean, decoupled systems by applying the 'accept interfaces, return structs' principle and interface composition.
  • Code Quality Audits: Identifying and refactoring anti-patterns such as global mutable state, naked returns, and improper resource management using defer.
  • Testing Strategy: Developing robust table-driven tests and utilizing test helpers to increase code coverage and reliability.
namego-best-practices
descriptionGo development best practices, patterns, and conventions. Use when writing Go code, reviewing .go files, discussing goroutines, channels, error handling, or Go project structure. Triggers on mentions of Go, Golang, goroutines, channels, defer, interfaces, go mod.

Go Best Practices

Project Structure

project/
├── cmd/
│   └── myapp/
│       └── main.go
├── internal/
│   ├── handler/
│   ├── service/
│   └── repository/
├── pkg/
│   └── shared/
├── go.mod
└── go.sum

Error Handling

Always Check Errors

// Bad
result, _ := doSomething()

// Good
result, err := doSomething()
if err != nil {
    return fmt.Errorf("doSomething failed: %w", err)
}

Wrap Errors with Context

func fetchUser(id string) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("fetch user %s: %w", id, err)
    }
    return user, nil
}

Sentinel Errors

var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

// Check with errors.Is
if errors.Is(err, ErrNotFound) {
    return http.StatusNotFound
}

Custom Error Types

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// Check with errors.As
var validErr *ValidationError
if errors.As(err, &validErr) {
    log.Printf("validation failed on %s", validErr.Field)
}

Concurrency

Goroutines with WaitGroups

var wg sync.WaitGroup
for _, item := range items {
    wg.Add(1)
    go func(item Item) {
        defer wg.Done()
        process(item)
    }(item)
}
wg.Wait()

Channels

// Buffered channel for bounded work
jobs := make(chan Job, 100)

// Signal-only channel
done := make(chan struct{})

// Close to broadcast
close(done)

Context for Cancellation

func fetchData(ctx context.Context, url string) error {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    return nil
}

Worker Pools

func worker(id int, jobs <-chan Job, results chan<- Result) {
    for job := range jobs {
        results <- process(job)
    }
}

func main() {
    jobs := make(chan Job, 100)
    results := make(chan Result, 100)

    // Start workers
    for w := 0; w < 3; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs
    for _, job := range allJobs {
        jobs <- job
    }
    close(jobs)
}

Interfaces

Small Interfaces

// Good - single method
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Accept interfaces, return structs
func Process(r Reader) (*Result, error) {
    // ...
}

Interface Composition

type ReadWriter interface {
    Reader
    Writer
}

Testing

Table-Driven Tests

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -2, -3},
        {"zero", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

Test Helpers

func setupTestDB(t *testing.T) *DB {
    t.Helper()
    db, err := NewDB(":memory:")
    if err != nil {
        t.Fatalf("setup db: %v", err)
    }
    t.Cleanup(func() { db.Close() })
    return db
}

Resource Management

Defer for Cleanup

func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    return io.ReadAll(f)
}

Mutex Patterns

type SafeCounter struct {
    mu    sync.RWMutex
    value int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *SafeCounter) Value() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.value
}

Naming Conventions

  • Packages: short, lowercase, no underscores (http, json)
  • Exported: PascalCase (NewServer, UserID)
  • Unexported: camelCase (userID, conn)
  • Acronyms: consistent case (URL, userID)
  • Getters: no Get prefix (user.Name() not user.GetName())

Common Patterns

Functional Options

type Server struct {
    port    int
    timeout time.Duration
}

type Option func(*Server)

func WithPort(p int) Option {
    return func(s *Server) { s.port = p }
}

func NewServer(opts ...Option) *Server {
    s := &Server{port: 8080, timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Constructor Pattern

func NewUser(name string) (*User, error) {
    if name == "" {
        return nil, errors.New("name required")
    }
    return &User{Name: name, CreatedAt: time.Now()}, nil
}

Anti-Patterns to Avoid

  • Naked returns in long functions
  • Ignoring errors with _
  • Passing large structs by value
  • Using init() for complex logic
  • Global mutable state
  • Goroutine leaks (always ensure cleanup)