golang-patterns

Idiomatic Go patterns, best practices, and conventions for building robust applications. Covers core principles including simplicity over cleverness, useful zero values, and accepting interfaces while returning concrete types Includes error handling patterns with wrapping, custom error types, and proper error checking using errors.Is and errors.As Provides concurrency patterns for worker pools, context-based cancellation, graceful shutdown, and goroutine leak prevention Addresses interface design, package organization, struct composition with functional options, and memory optimization techniques Features anti-patterns to avoid, Go tooling integration, and a quick reference of Go idioms

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill golang-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Go Development Patterns

Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.

When to Activate

  • Writing new Go code
  • Reviewing Go code
  • Refactoring existing Go code
  • Designing Go packages/modules

Core Principles

1. Simplicity and Clarity

Go favors simplicity over cleverness. Code should be obvious and easy to read.

// Good: Clear and direct

func GetUser(id string) (*User, error) {

    user, err := db.FindUser(id)

    if err != nil {

        return nil, fmt.Errorf("get user %s: %w", id, err)

    }

    return user, nil

}

// Bad: Overly clever

func GetUser(id string) (*User, error) {

    return func() (*User, error) {

        if u, e := db.FindUser(id); e == nil {

            return u, nil

        } else {

            return nil, e

        }

    }()

}

2. Make the Zero Value Useful

Design types so their zero value is immediately usable without initialization.

// Good: Zero value is useful

type Counter struct {

    mu    sync.Mutex

    count int // zero value is 0, ready to use

}

func (c *Counter) Inc() {

    c.mu.Lock()

    c.count++

    c.mu.Unlock()

}

// Good: bytes.Buffer works with zero value

var buf bytes.Buffer

buf.WriteString("hello")

// Bad: Requires initialization

type BadCounter struct {

    counts map[string]int // nil map will panic

}

3. Accept Interfaces, Return Structs

Functions should accept interface parameters and return concrete types.

// Good: Accepts interface, returns concrete type

func ProcessData(r io.Reader) (*Result, error) {

    data, err := io.ReadAll(r)

    if err != nil {

        return nil, err

    }

    return &Result{Data: data}, nil

}

// Bad: Returns interface (hides implementation details unnecessarily)

func ProcessData(r io.Reader) (io.Reader, error) {

    // ...

}

Error Handling Patterns

Error Wrapping with Context

// Good: Wrap errors with context

func LoadConfig(path string) (*Config, error) {

    data, err := os.ReadFile(path)

    if err != nil {

        return nil, fmt.Errorf("load config %s: %w", path, err)

    }

    var cfg Config

    if err := json.Unmarshal(data, &cfg); err != nil {

        return nil, fmt.Errorf("parse config %s: %w", path, err)

    }

    return &cfg, nil

}

Custom Error Types

// Define domain-specific errors

type ValidationError struct {

    Field   string

    Message string

}

func (e *ValidationError) Error() string {

    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)

}

// Sentinel errors for common cases

var (

    ErrNotFound     = errors.New("resource not found")

    ErrUnauthorized = errors.New("unauthorized")

    ErrInvalidInput = errors.New("invalid input")

)

Error Checking with errors.Is and errors.As

func HandleError(err error) {

    // Check for specific error

    if errors.Is(err, sql.ErrNoRows) {

        log.Println("No records found")

        return

    }

    // Check for error type

    var validationErr *ValidationError

    if errors.As(err, &validationErr) {

        log.Printf("Validation error on field %s: %s",

            validationErr.Field, validationErr.Message)

        return

    }

    // Unknown error

    log.Printf("Unexpected error: %v", err)

}

Never Ignore Errors

// Bad: Ignoring error with blank identifier

result, _ := doSomething()

// Good: Handle or explicitly document why it's safe to ignore

result, err := doSomething()

if err != nil {

    return err

}

// Acceptable: When error truly doesn't matter (rare)

_ = writer.Close() // Best-effort cleanup, error logged elsewhere

Concurrency Patterns

Worker Pool

func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {

    var wg sync.WaitGroup

    for i := 0; i < numWorkers; i++ {

        wg.Add(1)

        go func() {

            defer wg.Done()

            for job := range jobs {

                results <- process(job)

            }

        }()

    }

    wg.Wait()

    close(results)

}

Context for Cancellation and Timeouts

func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {

    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)

    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)

    if err != nil {

        return nil, fmt.Errorf("create request: %w", err)

    }

    resp, err := http.DefaultClient.Do(req)

    if err != nil {

        return nil, fmt.Errorf("fetch %s: %w", url, err)

    }

    defer resp.Body.Close()

    return io.ReadAll(resp.Body)

}

Graceful Shutdown

func GracefulShutdown(server *http.Server) {

    quit := make(chan os.Signal, 1)

    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    <-quit

    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)

    defer cancel()

    if err := server.Shutdown(ctx); err != nil {

        log.Fatalf("Server forced to shutdown: %v", err)

    }

    log.Println("Server exited")

}

errgroup for Coordinated Goroutines

import "golang.org/x/sync/errgroup"

func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {

    g, ctx := errgroup.WithContext(ctx)

    results := make([][]byte, len(urls))

    for i, url := range urls {

        i, url := i, url // Capture loop variables

        g.Go(func() error {

            data, err := FetchWithTimeout(ctx, url)

            if err != nil {

                return err

            }

            results[i] = data

            return nil

        })

    }

    if err := g.Wait(); err != nil {

        return nil, err

    }

    return results, nil

}

Avoiding Goroutine Leaks

// Bad: Goroutine leak if context is cancelled

func leakyFetch(ctx context.Context, url string) <-chan []byte {

    ch := make(chan []byte)

    go func() {

        data, _ := fetch(url)

        ch <- data // Blocks forever if no receiver

    }()

    return ch

}

// Good: Properly handles cancellation

func safeFetch(ctx context.Context, url string) <-chan []byte {

    ch := make(chan []byte, 1) // Buffered channel

    go func() {

        data, err := fetch(url)

        if err != nil {

            return

        }

        select {

        case ch <- data:

        case <-ctx.Done():

        }

    }()

    return ch

}

Interface Design

Small, Focused Interfaces

// Good: Single-method interfaces

type Reader interface {

    Read(p []byte) (n int, err error)

}

type Writer interface {

    Write(p []byte) (n int, err error)

}

type Closer interface {

    Close() error

}

// Compose interfaces as needed

type ReadWriteCloser interface {

    Reader

    Writer

    Closer

}

Define Interfaces Where They're Used

// In the consumer package, not the provider

package service

// UserStore defines what this service needs

type UserStore interface {

    GetUser(id string) (*User, error)

    SaveUser(user *User) error

}

type Service struct {

    store UserStore

}

// Concrete implementation can be in another package

// It doesn't need to know about this interface

Optional Behavior with Type Assertions

type Flusher interface {

    Flush() error

}

func WriteAndFlush(w io.Writer, data []byte) error {

    if _, err := w.Write(data); err != nil {

        return err

    }

    // Flush if supported

    if f, ok := w.(Flusher); ok {

        return f.Flush()

    }

    return nil

}

Package Organization

Standard Project Layout

myproject/

├── cmd/

│   └── myapp/

│       └── main.go           # Entry point

├── internal/

│   ├── handler/              # HTTP handlers

│   ├── service/              # Business logic

│   ├── repository/           # Data access

│   └── config/               # Configuration

├── pkg/

│   └── client/               # Public API client

├── api/

│   └── v1/                   # API definitions (proto, OpenAPI)

├── testdata/                 # Test fixtures

├── go.mod

├── go.sum

└── Makefile

Package Naming

// Good: Short, lowercase, no underscores

package http

package json

package user

// Bad: Verbose, mixed case, or redundant

package httpHandler

package json_parser

package userService // Redundant 'Service' suffix

Avoid Package-Level State

// Bad: Global mutable state

var db *sql.DB

func init() {

    db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))

}

// Good: Dependency injection

type Server struct {

    db *sql.DB

}

func NewServer(db *sql.DB) *Server {

    return &#x26;Server{db: db}

}

Struct Design

Functional Options Pattern

type Server struct {

    addr    string

    timeout time.Duration

    logger  *log.Logger

}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {

    return func(s *Server) {

        s.timeout = d

    }

}

func WithLogger(l *log.Logger) Option {

    return func(s *Server) {

        s.logger = l

    }

}

func NewServer(addr string, opts ...Option) *Server {

    s := &#x26;Server{

        addr:    addr,

        timeout: 30 * time.Second, // default

        logger:  log.Default(),    // default

    }

    for _, opt := range opts {

        opt(s)

    }

    return s

}

// Usage

server := NewServer(":8080",

    WithTimeout(60*time.Second),

    WithLogger(customLogger),

)

Embedding for Composition

type Logger struct {

    prefix string

}

func (l *Logger) Log(msg string) {

    fmt.Printf("[%s] %s\n", l.prefix, msg)

}

type Server struct {

    *Logger // Embedding - Server gets Log method

    addr    string

}

func NewServer(addr string) *Server {

    return &#x26;Server{

        Logger: &#x26;Logger{prefix: "SERVER"},

        addr:   addr,

    }

}

// Usage

s := NewServer(":8080")

s.Log("Starting...") // Calls embedded Logger.Log

Memory and Performance

Preallocate Slices When Size is Known

// Bad: Grows slice multiple times

func processItems(items []Item) []Result {

    var results []Result

    for _, item := range items {

        results = append(results, process(item))

    }

    return results

}

// Good: Single allocation

func processItems(items []Item) []Result {

    results := make([]Result, 0, len(items))

    for _, item := range items {

        results = append(results, process(item))

    }

    return results

}

Use sync.Pool for Frequent Allocations

var bufferPool = sync.Pool{

    New: func() interface{} {

        return new(bytes.Buffer)

    },

}

func ProcessRequest(data []byte) []byte {

    buf := bufferPool.Get().(*bytes.Buffer)

    defer func() {

        buf.Reset()

        bufferPool.Put(buf)

    }()

    buf.Write(data)

    // Process...

    return buf.Bytes()

}

Avoid String Concatenation in Loops

// Bad: Creates many string allocations

func join(parts []string) string {

    var result string

    for _, p := range parts {

        result += p + ","

    }

    return result

}

// Good: Single allocation with strings.Builder

func join(parts []string) string {

    var sb strings.Builder

    for i, p := range parts {

        if i > 0 {

            sb.WriteString(",")

        }

        sb.WriteString(p)

    }

    return sb.String()

}

// Best: Use standard library

func join(parts []string) string {

    return strings.Join(parts, ",")

}

Go Tooling Integration

Essential Commands

# Build and run

go build ./...

go run ./cmd/myapp

# Testing

go test ./...

go test -race ./...

go test -cover ./...

# Static analysis

go vet ./...

staticcheck ./...

golangci-lint run

# Module management

go mod tidy

go mod verify

# Formatting

gofmt -w .

goimports -w .

Recommended Linter Configuration (.golangci.yml)

linters:

  enable:

    - errcheck

    - gosimple

    - govet

    - ineffassign

    - staticcheck

    - unused

    - gofmt

    - goimports

    - misspell

    - unconvert

    - unparam

linters-settings:

  errcheck:

    check-type-assertions: true

  govet:

    check-shadowing: true

issues:

  exclude-use-default: false

Quick Reference: Go Idioms

Idiom

Description

Accept interfaces, return structs

Functions accept interface params, return concrete types

Errors are values

Treat errors as first-class values, not exceptions

Don't communicate by sharing memory

Use channels for coordination between goroutines

Make the zero value useful

Types should work without explicit initialization

A little copying is better than a little dependency

Avoid unnecessary external dependencies

Clear is better than clever

Prioritize readability over cleverness

gofmt is no one's favorite but everyone's friend

Always format with gofmt/goimports

Return early

Handle errors first, keep happy path unindented

Anti-Patterns to Avoid

// Bad: Naked returns in long functions

func process() (result int, err error) {

    // ... 50 lines ...

    return // What is being returned?

}

// Bad: Using panic for control flow

func GetUser(id string) *User {

    user, err := db.Find(id)

    if err != nil {

        panic(err) // Don't do this

    }

    return user

}

// Bad: Passing context in struct

type Request struct {

    ctx context.Context // Context should be first param

    ID  string

}

// Good: Context as first parameter

func ProcessRequest(ctx context.Context, id string) error {

    // ...

}

// Bad: Mixing value and pointer receivers

type Counter struct{ n int }

func (c Counter) Value() int { return c.n }    // Value receiver

func (c *Counter) Increment() { c.n++ }        // Pointer receiver

// Pick one style and be consistent

Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card