Golang Design Patterns
Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe
- Skill
- Guidelines
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive guide to idiomatic Go development. It emphasizes simplicity, explicitness, and testability, offering patterns for constructor APIs, resource management, error handling, and architectural organization. It helps developers choose the right level of abstraction and structure for their projects, ranging from simple scripts to complex, domain-driven services, while avoiding common pitfalls like global state and premature optimization.
What it does
- Provides idiomatic patterns for constructors, enums, and resource lifecycle management.
- Offers guidance on choosing architectural styles like Clean, Hexagonal, or DDD based on project scale.
- Defines best practices for resilience, including timeouts, retries, and resource limiting.
- Advises on Go-specific idioms like functional options, interface checks, and avoiding init() functions.
- Supports code reviews by identifying common design issues like unbounded resources or implicit dependencies.
- Promotes a 'fail-fast' approach and domain purity to ensure maintainable business logic.
When to use it
- When designing new Go APIs, packages, or application structures.
- When deciding on an architectural pattern for a new service.
- When auditing existing code for design flaws, hidden dependencies, or resource leaks.
- When looking for idiomatic ways to implement resilience, configuration, or data processing.
- When you need to ensure your Go code remains testable and decoupled from infrastructure.
Advantages
- Promotes consistent, idiomatic code across the codebase.
- Reduces technical debt by discouraging premature abstraction and hidden dependencies.
- Improves testability through dependency injection and interface-based design.
- Enhances system reliability with clear patterns for timeouts, retries, and graceful shutdowns.
- Scales architectural complexity appropriately to the project's actual needs.
Usage examples
- “How should I structure a constructor for a new service that needs several optional configurations?”
- The agent will recommend the functional options pattern, providing a code example that demonstrates how to define an Option type and a variadic constructor for clean, extensible API design.
- “I am building a medium-sized service; what architecture should I use?”
- The agent will ask about the project's scope and complexity, then suggest a layered architecture (handler/service/repository) or, if appropriate, a more formal approach like Clean or Hexagonal architecture.
- “Review this code for common Go design issues.”
- The agent will scan the provided code for anti-patterns such as init() abuse, missing timeouts on external calls, lack of resource cleanup, or unbounded resource usage.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/samber/golang-design-patterns/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip golang-design-patterns.zip -d .agents/skills
Documentation by samber
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
init()abuse, unbounded resources, missing timeouts, and implicit global state; report findings before suggesting refactors.
Community default. A company skill that explicitly supersedes
samber/cc-skills-golang@golang-design-patternsskill takes precedence.
Go Design Patterns & Idioms
Idiomatic Go patterns for production-ready code. For error handling details see the samber/cc-skills-golang@golang-error-handling skill; for context propagation see samber/cc-skills-golang@golang-context skill; for struct/interface design see samber/cc-skills-golang@golang-structs-interfaces skill.
Best 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
init()— runs implicitly, cannot return errors, makes testing unpredictable. Use explicit constructorsEnums 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 cleanupruntime.AddCleanupoverruntime.SetFinalizer— finalizers are unpredictable and can resurrect objectsEvery 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.Builderfor concatenation in loops → seesamber/cc-skills-golang@golang-code-stylestring vs []byte: use
[]bytefor mutation and I/O,stringfor display and keys — conversions allocateIterators (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:embedfor static assets — embeds at compile time, eliminates runtime file I/O errorsUse
crypto/randfor keys/tokens —math/randis predictable → seesamber/cc-skills-golang@golang-securityRegexp 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 — fragileCannot return errors — failures must panic or
log.FatalRuns 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 stringCompile-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 usedefer 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-structuresskill for data structure selection, internals, and container/ packages→ See
samber/cc-skills-golang@golang-error-handlingskill for error wrapping, sentinel errors, and the single handling rule→ See
samber/cc-skills-golang@golang-structs-interfacesskill for interface design and composition→ See
samber/cc-skills-golang@golang-concurrencyskill for goroutine lifecycle and graceful shutdown→ See
samber/cc-skills-golang@golang-contextskill for timeout and cancellation patterns→ See
samber/cc-skills-golang@golang-project-layoutskill for architecture and directory structure→ See
samber/cc-skills-golang@golang-refactoringskill for safely staging a migration toward one of these patterns (options struct, DI, consumer-side interfaces) across an existing codebase
Files
- ATTRIBUTION.mdAdded by AutoGineer923 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md11.8 KiB
- evals/evals.json22.1 KiB
- references/architecture.md5.1 KiB
- references/clean-architecture.md5.6 KiB
- references/data-handling.md1.6 KiB
- references/ddd.md7.3 KiB
- references/hexagonal-architecture.md6.8 KiB
- references/resource-management.md4.3 KiB
references/hexagonal-architecture.md
Hexagonal Architecture (Ports & Adapters) in Go
Table of Contents
When to Use
Core Concepts
Project Structure
Code Examples
Domain — pure business logic
Ports — interfaces defined separately from implementations
Service — implements primary port, depends on secondary ports
Primary Adapter — HTTP handler calls the service port
Secondary Adapter — implements a driven port
Multiple Entry Points
Wiring
When to Use
Apply hexagonal architecture when a service interacts with multiple external systems (databases, APIs, message queues, caches) and you want the domain logic fully decoupled from all of them. Particularly effective when the same business logic needs multiple entry points (HTTP, gRPC, CLI, message consumer). Do NOT use for simple CRUD apps or libraries.
Core Concepts
Domain — Business logic and types. No external dependencies.
Ports — Interfaces that define how the domain interacts with the outside world.
Primary (driving) ports: How the outside world calls into the domain (e.g.,
OrderServiceinterface).Secondary (driven) ports: How the domain calls out to infrastructure (e.g.,
OrderRepository,PaymentGatewayinterfaces).
Adapters — Concrete implementations of ports.
Primary adapters: HTTP handlers, gRPC servers, CLI commands — they call primary ports.
Secondary adapters: PostgreSQL repository, Stripe client, Redis cache — they implement secondary ports.
Project Structure
order-service/
├── cmd/
│ ├── server/
│ │ └── main.go # HTTP server wiring
│ └── worker/
│ └── main.go # Message consumer wiring
├── internal/
│ ├── domain/
│ │ ├── order.go # Order entity + business rules
│ │ ├── item.go # OrderItem
│ │ └── status.go # OrderStatus enum
│ ├── port/
│ │ ├── incoming.go # Primary ports (OrderService interface)
│ │ └── outgoing.go # Secondary ports (OrderRepository, PaymentGateway)
│ ├── service/
│ │ └── order_service.go # Implements primary ports — orchestrates domain + secondary ports
│ └── adapter/
│ ├── primary/
│ │ ├── http/
│ │ │ ├── router.go
│ │ │ └── order_handler.go # HTTP adapter — calls OrderService
│ │ └── grpc/
│ │ └── order_server.go # gRPC adapter — calls OrderService
│ └── secondary/
│ ├── postgres/
│ │ └── order_repo.go # Implements OrderRepository
│ └── stripe/
│ └── payment.go # Implements PaymentGateway
├── go.mod
└── go.sumCode Examples
Domain — pure business logic
// internal/domain/order.go
package domain
type Order struct {
ID string
Items []Item
Status OrderStatus
}
func (o *Order) Ship() error {
if o.Status != StatusPaid {
return ErrOrderNotPaid
}
o.Status = StatusShipped
return nil
}Ports — interfaces defined separately from implementations
// internal/port/incoming.go
package port
// Primary port — how the outside world drives the application
type OrderService interface {
PlaceOrder(ctx context.Context, items []domain.Item) (string, error)
ShipOrder(ctx context.Context, orderID string) error
GetOrder(ctx context.Context, orderID string) (*domain.Order, error)
}// internal/port/outgoing.go
package port
// Secondary ports — how the application reaches external systems
type OrderRepository interface {
Save(ctx context.Context, order *domain.Order) error
FindByID(ctx context.Context, id string) (*domain.Order, error)
}
type PaymentGateway interface {
Charge(ctx context.Context, orderID string, amount int64) error
}Service — implements primary port, depends on secondary ports
// internal/service/order_service.go
package service
type orderService struct {
orders port.OrderRepository
payments port.PaymentGateway
}
func NewOrderService(orders port.OrderRepository, payments port.PaymentGateway) port.OrderService {
return &orderService{orders: orders, payments: payments}
}
func (s *orderService) PlaceOrder(ctx context.Context, items []domain.Item) (string, error) {
order := domain.NewOrder(items)
if err := s.payments.Charge(ctx, order.ID, order.Total()); err != nil {
return "", fmt.Errorf("charging payment: %w", err)
}
if err := s.orders.Save(ctx, order); err != nil {
return "", fmt.Errorf("saving order: %w", err)
}
return order.ID, nil
}Primary Adapter — HTTP handler calls the service port
// internal/adapter/primary/http/order_handler.go
package http
type OrderHandler struct {
svc port.OrderService
}
func NewOrderHandler(svc port.OrderService) *OrderHandler {
return &OrderHandler{svc: svc}
}
func (h *OrderHandler) HandlePlaceOrder(w http.ResponseWriter, r *http.Request) {
var req PlaceOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
id, err := h.svc.PlaceOrder(r.Context(), req.Items)
if err != nil {
http.Error(w, err.Error(), mapToHTTPStatus(err))
return
}
json.NewEncoder(w).Encode(map[string]string{"id": id})
}Secondary Adapter — implements a driven port
// internal/adapter/secondary/postgres/order_repo.go
package postgres
type OrderRepo struct {
db *sql.DB
}
func NewOrderRepo(db *sql.DB) *OrderRepo {
return &OrderRepo{db: db}
}
func (r *OrderRepo) Save(ctx context.Context, order *domain.Order) error {
// SQL upsert
}
func (r *OrderRepo) FindByID(ctx context.Context, id string) (*domain.Order, error) {
// SQL query
}Multiple Entry Points
The hexagonal approach shines when the same OrderService is called from different primary adapters — HTTP for external clients, gRPC for internal services, a message consumer for async events. Each adapter is wired in its own cmd/ entry point.
Wiring
Construct adapters and inject them in cmd/server/main.go. → See samber/cc-skills-golang@golang-dependency-injection skill for DI library alternatives.
Requirements and compatibility
Written for: Claude Code, OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Claude Code frontmatter
The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- samber
- Publisher
- Samuel Berthe (Community)
- Distributed by
- Samuel Berthe
- Source
- samber/cc-skills-golang · skills/golang-design-patterns · commit bb1d43d
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-design-patterns.zip