Skip to content
Development
Skill

/golang-idioms

Idiomatic Go patterns for error handling, interfaces, concurrency, testing, and module management

From plugin
rohitg00-claude-code-toolkit
2.5k40 skills138 agents199 commands
Install
$ npx -y skills add rohitg00/awesome-claude-code-toolkit --skill golang-idioms --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/golang-idioms

Context preview

The summary Claude sees to decide when to auto-load this skill.

Idiomatic Go patterns for error handling, interfaces, concurrency, testing, and module management

SKILL.md

golang-idioms.SKILL.md
name: golang-idioms
description: Idiomatic Go patterns for error handling, interfaces, concurrency, testing, and module management

Go Idioms

Error Handling

// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("reading config %s: %w", path, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return Config{}, fmt.Errorf("parsing config: %w", err)
    }

    return cfg, nil
}

Rules:

  • Always wrap errors with context using `fmt.Errorf("context: %w", err)`
  • Use `%w` to allow callers to use `errors.Is` and `errors.As`
  • Handle errors at the appropriate level; do not log and return the same error
  • Define sentinel errors for expected conditions
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

func GetUser(id string) (User, error) {
    user, ok := store[id]
    if !ok {
        return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    return user, nil
}

// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "user not found", http.StatusNotFound)
    return
}

Interface Design

// Keep interfaces small (1-3 methods)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type UserStore interface {
    GetUser(ctx context.Context, id string) (User, error)
    CreateUser(ctx context.Context, u User) error
}

// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
    return &Service{store: store, logger: logger}
}

Rules:

  • Define interfaces where they are used (consumer side), not where they are implemented
  • Prefer small, composable interfaces over large ones
  • Use `io.Reader`, `io.Writer`, `fmt.Stringer` from the standard library
  • An interface with one method should be named after the method + `er` suffix

Goroutine and Channel Patterns

Worker Pool

func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
    results := make(chan Result, workers)
    var wg sync.WaitGroup

    for range workers {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                select {
                case <-ctx.Done():
                    return
                case results <- job.Execute():
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    return results
}

Fan-out/Fan-in

func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
    sem := make(chan struct{}, concurrency)
    results := make([]R, len(items))
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        sem <- struct{}{}
        go func() {
            defer func() { <-sem; wg.Done() }()
            results[i] = fn(item)
        }()
    }

    wg.Wait()
    return results
}

Rules:

  • Always pass `context.Context` as the first parameter
  • Always ensure goroutines can be stopped (via context cancellation or channel close)
  • Use `sync.WaitGroup` to wait for goroutine completion
  • Use buffered channels when producer and consumer run at different speeds
  • Never start a goroutine without knowing how it will stop

Context Propagation

func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    user, err := s.store.GetUser(ctx, req.UserID)
    if err != nil {
        return Response{}, fmt.Errorf("getting user: %w", err)
    }

    ctx = context.WithValue(ctx, userKey, user)
    return s.processRequest(ctx, req)
}

Rules:

  • Pass context as the first parameter of every function that does I/O
  • Use `context.WithTimeout` or `context.WithDeadline` for all external calls
  • Always `defer cancel()` after creating a cancellable context
  • Use `context.WithValue` sparingly (request-scoped values only: trace IDs, auth info)
  • Never store context in a struct

Table-Driven Tests

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"valid email", "user@example.com", true},
        {"missing @", "userexample.com", false},
        {"empty string", "", false},
        {"multiple @", "user@@example.com", false},
        {"valid with subdomain", "user@mail.example.com", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := ValidateEmail(tt.email)
            if got != tt.want {
                t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
            }
        })
    }
}

Test Helpers

func newTestServer(t *testing.T) *httptest.Server {
    t.Helper()
    handler := setupRoutes()
    srv := httptest.NewServer(handler)
    t.Cleanup(srv.Close)
    return srv
}

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

Use `t.Helper()` in all test utility functions. Use `t.Cleanup()` instead of `defer` for test resource cleanup. Use `testdata/` directory for test fixtures.

Module Management

go.mod structure:
module github.com/org/project

go 1.23

require (
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.7.0
)

Commands:

go mod tidy          # remove unused, add missing
go mod verify        # verify checksums
go list -m -u all    # check for updates
go get -u ./...      # update all dependencies
go mod vendor        # vendor dependencies (optional)

Use `go mod tidy` before every commit. Pin major versions. Review changelogs before updating.

Zero-Value Design

Design types so their zero value is useful:

// sync.Mutex zero value is
Read more
Ships withrohitg00-claude-code-toolkit

The most comprehensive toolkit for Claude Code -- 135 agents, 35 curated skills (+400,000 via SkillKit), 42 commands, 176+ plugins, 20 hooks, 15 rules, 7 templates, 15 MCP configs, 26 companion apps, 53 ecosystem entries, and more.

Get the whole plugin