Go Patterns Basics
10 examples to get you started with Go patterns and idioms - 7 basic and 3 intermediate.
Prerequisites
- Go 1.26.x installed (
go versionreportsgo1.26or later) - Familiarity with structs, methods, and interfaces
go versionBasic Examples
1. Embed to Promote Methods
Compose behavior by embedding a type with the methods you need.
type LoggingHandler struct {
http.Handler
log *slog.Logger
}
func (h LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.log.Info("request", "path", r.URL.Path)
h.Handler.ServeHTTP(w, r)
}- Anonymous embedding promotes
ServeHTTPfrom the inner handler - The outer type is not a subclass - it is a distinct struct wrapping behavior
- Use embedding for thin decorators, not deep hierarchies
Related: Go Idioms: Composition Over Inheritance - why Go avoids inheritance
2. Define a Consumer Interface
Declare the interface where you call the behavior, not where you implement it.
// consumer package
type Clock interface {
Now() time.Time
}
func TTL(c Clock, issued time.Time, ttl time.Duration) bool {
return c.Now().After(issued.Add(ttl))
}- Interfaces are satisfied implicitly when methods match
- Small interfaces (one method) are easy to fake in tests
- Keep names role-based (
Clock,Store) not implementation-based (PostgresClock)
Related: Constructor Functions & Package APIs - wiring dependencies through constructors
3. Constructor with Unexported State
Return initialized values from New functions instead of exporting mutable structs.
type Counter struct {
n atomic.Int64
}
func NewCounter() *Counter { return &Counter{} }
func (c *Counter) Inc() { c.n.Add(1) }
func (c *Counter) Value() int64 { return c.n.Load() }Newdocuments valid construction; zero value may be invalid for some types- Unexported fields prevent external mutation breaking invariants
- Methods on pointer receivers when state mutates
Related: Constructor Functions & Package APIs - New/Open conventions
4. Functional Option Type
Model optional settings as functions that mutate the target during construction.
type Option func(*Server)
func WithPort(p int) Option {
return func(s *Server) { s.port = p }
}
func NewServer(opts ...Option) *Server {
s := &Server{port: 8080}
for _, opt := range opts {
opt(s)
}
return s
}- Defaults live in
New; callers override only what they need - Options compose:
NewServer(WithPort(3000), WithTLS(cert)) - Export option functions, keep the option type alias unexported if possible
Related: Functional Options Pattern - full pattern walkthrough
5. HTTP Middleware Chain
Wrap handlers with functions that run before and after inner logic.
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.NewString()
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-Id", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}- Middleware signature
func(http.Handler) http.Handleris stdlib-native - Outer middleware runs first on the way in, last on the way out
- chi, gin, and echo use the same composition idea with router-specific helpers
Related: Middleware & Decorator Patterns - chaining and ordering
6. Repository Interface Boundary
Isolate storage behind an interface the service depends on.
type User struct { ID string; Email string }
type UserRepo interface {
Get(ctx context.Context, id string) (User, error)
}
type Service struct { repo UserRepo }
func (s *Service) Email(ctx context.Context, id string) (string, error) {
u, err := s.repo.Get(ctx, id)
if err != nil { return "", err }
return u.Email, nil
}- Services hold business rules; repositories hold I/O details
- Tests pass an in-memory
UserRepofake without a database - Context is the first parameter on I/O boundaries
Related: Repository & Service Layer Patterns - layering guidance
7. sync.Once for Lazy Init
Initialize expensive resources exactly once, safely under concurrency.
var (
client *http.Client
once sync.Once
)
func HTTPClient() *http.Client {
once.Do(func() {
client = &http.Client{Timeout: 10 * time.Second}
})
return client
}once.Doruns the function exactly one time across goroutines- Prefer explicit dependency injection over package globals when tests allow
- Do not put network dials inside
init()- lazy init or constructor wiring is safer
Related: sync.Once & Singleton Patterns - when globals are acceptable
Intermediate Examples
8. Functional Options with Validation
Validate composed options before returning the constructed value.
func NewPool(size int, opts ...PoolOption) (*Pool, error) {
if size <= 0 {
return nil, errors.New("pool: size must be positive")
}
p := &Pool{size: size, idle: 30 * time.Second}
for _, opt := range opts {
if err := opt(p); err != nil {
return nil, err
}
}
return p, nil
}- Returning
errorfromNewsignals invalid combinations early - Option functions can return
errorwhen validation is per-option - Callers handle construction failures like any other Go API
Related: Functional Options Pattern - validation strategies
9. Service Constructor with Interfaces
Wire a service from interfaces defined in the consumer package.
type Notifier interface {
Send(ctx context.Context, to, body string) error
}
type OrderService struct {
repo OrderRepo
notify Notifier
}
func NewOrderService(repo OrderRepo, notify Notifier) *OrderService {
return &OrderService{repo: repo, notify: notify}
}- Constructor parameters document dependencies explicitly
- No hidden globals; tests pass
fakeNotifierandfakeRepo - Return concrete
*OrderService; accept interfaces in parameters
Related: Repository & Service Layer Patterns - testing with fakes
10. Middleware Stack with chi
Compose stdlib-compatible middleware on a chi router.
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})r.Useregisters outer middleware for all routes in scope- Route groups apply middleware to subtrees without duplicating wrappers
- chi stays
net/httpcompatible - handlers are plainhttp.HandlerFunc
Related: Middleware & Decorator Patterns - framework-neutral chaining
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).