golang-design-patterns

Idiomatic Golang design patterns — functional options, constructors, error flow and cascading, resource management and lifecycle, graceful shutdown,…

INSTALLATION
npx skills add https://github.com/samber/cc-skills-golang --skill golang-design-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$28

  • Constructors SHOULD use functional options — they scale better as APIs evolve (one function per option, no breaking changes)
  • Functional options MUST return an error if validation can fail — catch bad config at construction, not at runtime
  • **Avoid init()** — runs implicitly, cannot return errors, makes testing unpredictable. Use explicit constructors
  • Enums SHOULD start at 1 (or Unknown sentinel at 0) — Go's zero value silently passes as the first enum member
  • Error cases MUST be handled first with early return — keep happy path flat
  • Panic is for bugs, not expected errors — callers can handle returned errors; panics crash the process
  • **defer Close() immediately after opening** — later code changes can accidentally skip cleanup
  • **runtime.AddCleanup** over runtime.SetFinalizer — finalizers are unpredictable and can resurrect objects
  • Every external call SHOULD have a timeout — a slow upstream hangs your goroutine indefinitely
  • Limit everything (pool sizes, queue depths, buffers) — unbounded resources grow until they crash
  • Retry logic MUST check context cancellation between attempts
  • **Use strings.Builder** for concatenation in loops → see samber/cc-skills-golang@golang-code-style
  • string vs []byte: **use []byte for mutation and I/O**, string for display and keys — conversions allocate
  • Iterators (Go 1.23+): use for lazy evaluation — avoid loading everything into memory
  • Stream large transfers — loading millions of rows causes OOM; stream keeps memory constant
  • //go:embed for static assets — embeds at compile time, eliminates runtime file I/O errors
  • **Use crypto/rand** for keys/tokens — math/rand is predictable → see samber/cc-skills-golang@golang-security
  • Regexp MUST be compiled once at package level — compilation is O(n) and allocates
  • Compile-time interface checks: **var _ Interface = (*Type)(nil)**
  • A little recode > a big dependency — each dep adds attack surface and maintenance burden
  • Design for testability — accept interfaces, inject dependencies

Constructor Patterns: Functional Options vs Builder

Functional Options (Preferred)

type Server struct {

    addr         string

    readTimeout  time.Duration

    writeTimeout time.Duration

    maxConns     int

}

type Option func(*Server)

func WithReadTimeout(d time.Duration) Option {

    return func(s *Server) { s.readTimeout = d }

}

func WithWriteTimeout(d time.Duration) Option {

    return func(s *Server) { s.writeTimeout = d }

}

func WithMaxConns(n int) Option {

    return func(s *Server) { s.maxConns = n }

}

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

    // Default options

    s := &Server{

        addr:         addr,

        readTimeout:  5 * time.Second,

        writeTimeout: 10 * time.Second,

        maxConns:     100,

    }

    for _, opt := range opts {

        opt(s)

    }

    return s

}

// Usage

srv := NewServer(":8080",

    WithReadTimeout(30*time.Second),

    WithMaxConns(500),

)

Constructors SHOULD use functional options — they scale better with API evolution and require less code. Use builder pattern only if you need complex validation between configuration steps.

Constructors & Initialization

Avoid init() and Mutable Globals

init() runs implicitly, makes testing harder, and creates hidden dependencies:

  • Multiple init() functions run in declaration order, across files in filename alphabetical order — fragile
  • Cannot return errors — failures must panic or log.Fatal
  • Runs before main() and tests — side effects make tests unpredictable
// Bad — hidden global state

var db *sql.DB

func init() {

    var err error

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

    if err != nil {

        log.Fatal(err)

    }

}

// Good — explicit initialization, injectable

func NewUserRepository(db *sql.DB) *UserRepository {

    return &UserRepository{db: db}

}

Enums: Start at 1

Zero values should represent invalid/unset state:

type Status int

const (

    StatusUnknown Status = iota // 0 = invalid/unset

    StatusActive                // 1

    StatusInactive              // 2

    StatusSuspended             // 3

)

Compile Regexp Once

// Good — compiled once at package level

var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

func ValidateEmail(email string) bool {

    return emailRegex.MatchString(email)

}

Use //go:embed for Static Assets

import "embed"

//go:embed templates/*

var templateFS embed.FS

//go:embed version.txt

var version string

Compile-Time Interface Checks

→ See samber/cc-skills-golang@golang-structs-interfaces for the var _ Interface = (*Type)(nil) pattern.

Error Flow Patterns

Error cases MUST be handled first with early return — keep the happy path at minimal indentation. → See samber/cc-skills-golang@golang-code-style for the full pattern and examples.

When to Panic vs Return Error

  • Return error: network failures, file not found, invalid input — anything a caller can handle
  • Panic: nil pointer in a place that should be impossible, violated invariant, Must* constructors used at init time
  • **.Close() / Flush() errors**: read-only cleanup can often use defer f.Close(), but write/flush resources must report close or flush errors when durability matters

Data Handling

string vs []byte vs []rune

Type

Default for

Use when

string

Everything

Immutable, safe, UTF-8

[]byte

I/O

Writing to io.Writer, building strings, mutations

[]rune

Unicode ops

len() must mean characters, not bytes

Avoid repeated conversions — each one allocates. Stay in one type until you need the other.

Iterators & Streaming for Large Data

Use iterators (Go 1.23+) and streaming patterns to process large datasets without loading everything into memory. For large transfers between services (e.g., 1M rows DB to HTTP), stream to prevent OOM.

For code examples, see Data Handling Patterns.

Resource Management

defer Close() immediately after opening — don't wait, don't forget:

f, err := os.Open(path)

if err != nil {

    return err

}

defer f.Close() // right here, not 50 lines later

rows, err := db.QueryContext(ctx, query)

if err != nil {

    return err

}

defer rows.Close()

For graceful shutdown, resource pools, and runtime.AddCleanup, see Resource Management.

Resilience & Limits

Timeout Every External Call

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

defer cancel()

resp, err := httpClient.Do(req.WithContext(ctx))

Retry & Context Checks

Retry logic MUST check ctx.Err() between attempts and use exponential/linear backoff via select on ctx.Done(). Long loops MUST check ctx.Err() periodically. → See samber/cc-skills-golang@golang-context skill.

Database Patterns

→ See samber/cc-skills-golang@golang-database skill for sqlx/pgx, transactions, nullable columns, connection pools, repository interfaces, testing.

Architecture

Ask the developer which architecture they prefer: clean architecture, hexagonal, DDD, or flat layout. Don't impose complex architecture on a small project.

Core principles regardless of architecture:

  • Keep domain pure — no framework dependencies in the domain layer
  • Fail fast — validate at boundaries, trust internal code
  • Make illegal states unrepresentable — use types to enforce invariants
  • Respect 12-factor app principles — → see samber/cc-skills-golang@golang-project-layout

Detailed Guides

Guide

Scope

Architecture Patterns

High-level principles, when each architecture fits

Clean Architecture

Use cases, dependency rule, layered adapters

Hexagonal Architecture

Ports and adapters, domain core isolation

Domain-Driven Design

Aggregates, value objects, bounded contexts

Code Philosophy

  • Avoid repetitive code — but don't abstract prematurely
  • Minimize dependencies — a little recode > a big dependency
  • Design for testability — accept interfaces, inject dependencies, keep functions pure

Cross-References

  • → See samber/cc-skills-golang@golang-data-structures skill for data structure selection, internals, and container/ packages
  • → See samber/cc-skills-golang@golang-error-handling skill for error wrapping, sentinel errors, and the single handling rule
  • → See samber/cc-skills-golang@golang-structs-interfaces skill for interface design and composition
  • → See samber/cc-skills-golang@golang-concurrency skill for goroutine lifecycle and graceful shutdown
  • → See samber/cc-skills-golang@golang-context skill for timeout and cancellation patterns
  • → See samber/cc-skills-golang@golang-project-layout skill for architecture and directory structure
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