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-patterns
skill 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 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()
errors
acceptable to not check —
defer f.Close()
is fine without error handling
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