golang-design-patterns
Original:🇺🇸 English
Translated
Idiomatic Golang design patterns — functional options, constructors, error flow and cascading, resource management and lifecycle, graceful shutdown, resilience, architecture, dependency injection, data handling, and streaming. Apply when designing Go APIs, structuring applications, choosing between patterns, making design decisions, architectural choices, or production hardening.
10installs
Sourcesamber/cc-skills-golang
Added on
NPX Install
npx skill4agent add samber/cc-skills-golang golang-design-patternsTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Persona: You are a Go architect who values simplicity and explicitness. You apply patterns only when they solve a real problem — not to demonstrate sophistication — and you push back on premature abstraction.
Modes:
- Design mode — creating new APIs, packages, or application structure: ask the developer about their architecture preference before proposing patterns; favor the smallest pattern that satisfies the requirement.
- Review mode — auditing existing code for design issues: scan for abuse, unbounded resources, missing timeouts, and implicit global state; report findings before suggesting refactors.
init()
Community default. A company skill that explicitly supersedesskill takes precedence.samber/cc-skills-golang@golang-design-patterns
Go Design Patterns & Idioms
Idiomatic Go patterns for production-ready code. For error handling details see the skill; for context propagation see skill; for struct/interface design see skill.
samber/cc-skills-golang@golang-error-handlingsamber/cc-skills-golang@golang-contextsamber/cc-skills-golang@golang-structs-interfacesBest Practices Summary
- 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 — runs implicitly, cannot return errors, makes testing unpredictable. Use explicit constructors
init() - 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
- immediately after opening — later code changes can accidentally skip cleanup
defer Close() - over
runtime.AddCleanup— finalizers are unpredictable and can resurrect objectsruntime.SetFinalizer - 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 for concatenation in loops → see
strings.Buildersamber/cc-skills-golang@golang-code-style - string vs []byte: use for mutation and I/O,
[]bytefor display and keys — conversions allocatestring - 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
- for static assets — embeds at compile time, eliminates runtime file I/O errors
//go:embed - Use for keys/tokens —
crypto/randis predictable → seemath/randsamber/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)
go
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()init()- Multiple functions run in declaration order, across files in filename alphabetical order — fragile
init() - Cannot return errors — failures must panic or
log.Fatal - Runs before and tests — side effects make tests unpredictable
main()
go
// 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:
go
type Status int
const (
StatusUnknown Status = iota // 0 = invalid/unset
StatusActive // 1
StatusInactive // 2
StatusSuspended // 3
)Compile Regexp Once
go
// 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
//go:embedgo
import "embed"
//go:embed templates/*
var templateFS embed.FS
//go:embed version.txt
var version stringCompile-Time Interface Checks
→ See for the pattern.
samber/cc-skills-golang@golang-structs-interfacesvar _ Interface = (*Type)(nil)Error Flow Patterns
Error cases MUST be handled first with early return — keep the happy path at minimal indentation. → See for the full pattern and examples.
samber/cc-skills-golang@golang-code-styleWhen 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, constructors used at init time
Must* - errors: acceptable to not check —
.Close()is fine without error handlingdefer f.Close()
Data Handling
string vs []byte vs []rune
| Type | Default for | Use when |
|---|---|---|
| Everything | Immutable, safe, UTF-8 |
| I/O | Writing to |
| Unicode ops | |
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()go
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 , see Resource Management.
runtime.AddCleanupResilience & Limits
Timeout Every External Call
go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := httpClient.Do(req.WithContext(ctx))Retry & Context Checks
Retry logic MUST check between attempts and use exponential/linear backoff via on . Long loops MUST check periodically. → See skill.
ctx.Err()selectctx.Done()ctx.Err()samber/cc-skills-golang@golang-contextDatabase Patterns
→ See skill for sqlx/pgx, transactions, nullable columns, connection pools, repository interfaces, testing.
samber/cc-skills-golang@golang-databaseArchitecture
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 skill for data structure selection, internals, and container/ packages
samber/cc-skills-golang@golang-data-structures - → See skill for error wrapping, sentinel errors, and the single handling rule
samber/cc-skills-golang@golang-error-handling - → See skill for interface design and composition
samber/cc-skills-golang@golang-structs-interfaces - → See skill for goroutine lifecycle and graceful shutdown
samber/cc-skills-golang@golang-concurrency - → See skill for timeout and cancellation patterns
samber/cc-skills-golang@golang-context - → See skill for architecture and directory structure
samber/cc-skills-golang@golang-project-layout