Core Go development patterns and idioms. Use for package design, code organization, configuration patterns, interface definitions, or when editing .go files...
Configuration packages serve as ephemeral data containers that transform into domain objects at package boundaries.
Key Pattern:
// Configuration: structure and defaults only
type ServerConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
Timeout time.Duration `toml:"timeout"`
}
// Transformation: config ā domain object
func NewServer(cfg ServerConfig) (*Server, error) {
if cfg.Port == 0 {
cfg.Port = 8080 // Default
}
return &Server{
addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
timeout: cfg.Timeout,
}, nil
}
Decision Framework:
Never expose direct field access to nested structures; always provide semantic getter methods.
Bad:
// Exposes internal structure, fragile to changes
chunk.Choices[0].Delta.Content
Good:
// Semantic getter encapsulates access logic
func (c *Chunk) ExtractContent() string {
if len(c.Choices) == 0 {
return ""
}
return c.Choices[0].Delta.Content
}
// Usage
content := chunk.ExtractContent()
Benefits:
Structure code within files in dependency order: foundational types first.
Order:
package example
import "context"
// Constants
const DefaultTimeout = 30 * time.Second
// Interfaces
type Repository interface {
Find(ctx context.Context, id string) (*Entity, error)
}
// Pure types
type EntityType string
const (
TypeA EntityType = "a"
TypeB EntityType = "b"
)
// Structures with methods
type Entity struct {
ID string
Type EntityType
}
func (e *Entity) Validate() error {
if e.ID == "" {
return errors.New("id required")
}
return nil
}
// Standalone functions
func NewEntity(id string, t EntityType) *Entity {
return &Entity{ID: id, Type: t}
}
If a function requires more than 2 parameters, encapsulate them into a structure.
Bad:
func Execute(ctx context.Context, capability string, input string,
timeout time.Duration, retries int, cache bool) (*Result, error)
Good:
type ExecuteRequest struct {
Capability string
Input string
Timeout time.Duration
Retries int
UseCache bool
}
func Execute(ctx context.Context, req ExecuteRequest) (*Result, error)
Benefits:
Layers should interconnect exclusively through interfaces, not concrete types.
// Interface defines public API
type Renderer interface {
Render(input []byte) ([]byte, error)
}
// Constructor returns interface, not concrete type
func NewImageMagickRenderer(cfg ImageConfig) (Renderer, error) {
return &imageMagickRenderer{cfg: cfg}, nil
}
// Consumer stores interface dependency
type PDFDocument struct {
renderer Renderer // Interface, not *imageMagickRenderer
}
func NewPDFDocument(r Renderer) *PDFDocument {
return &PDFDocument{renderer: r}
}
Maintain clear, unidirectional dependencies flowing from high-level to low-level packages.
Level 0: observability/ (no dependencies)
ā
Level 1: messaging/ (depends on observability)
ā
Level 2: hub/ (depends on messaging)
ā
Level 3: state/ (depends on observability)
ā
Level 4: workflows/ (depends on state + observability)
Rules:
Avoid package subdirectories deeper than one level.
Good:
pkg/
āāā image/
āāā cache/
āāā document/
Bad:
pkg/
āāā document/
āāā formats/
āāā processors/
āāā types/
Deep nesting signals architectural problems. Ask: Should this be a separate package?
Lower-level packages define minimal interfaces that higher-level packages implement.
// In pkg/cache (lower level)
type Logger interface {
Log(ctx context.Context, level string, msg string)
}
// Cache uses the interface, doesn't import logger package
type Cache struct {
logger Logger
}
func NewCache(logger Logger) *Cache {
return &Cache{logger: logger}
}
// In pkg/logger (higher level) - implements the contract
type SlogAdapter struct {
slog *slog.Logger
}
func (a *SlogAdapter) Log(ctx context.Context, level, msg string) {
a.slog.Log(ctx, parseLevel(level), msg)
}
// Usage: dependency injection
cache := NewCache(&SlogAdapter{slog: slog.Default()})
Leverage latest language features and standard library methods.
// sync.WaitGroup.Go() - Combines Add(1) + goroutine launch + implicit Done()
var wg sync.WaitGroup
for _, task := range tasks {
wg.Go(func() {
process(task)
})
}
wg.Wait()
// for range n - Integer range without index variable
workers := min(runtime.NumCPU()*2, len(tasks))
for range workers {
wg.Go(func() { /* worker */ })
}
// min()/max() built-ins
limit := min(requested, maxAllowed)
// errors.Join() - Combine multiple errors
var errs []error
for _, item := range items {
if err := validate(item); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
// defer close(channel) - Always in sender goroutine
go func() {
defer close(results)
for item := range input {
results <- process(item)
}
}()
// Bad: Config persists and is accessed at runtime
type Service struct {
config Config // Stored and used later
}
func (s *Service) Process() {
timeout := s.config.Timeout // Accessing config at runtime
}
// Good: Config transformed at construction
type Service struct {
timeout time.Duration // Only the needed value stored
}
func NewService(cfg Config) *Service {
return &Service{timeout: cfg.Timeout}
}
// Bad: Package does too many things
package utils
func ParseJSON() {}
func SendEmail() {}
func ResizeImage() {}
func ValidateInput() {}
// Good: Single responsibility packages
package json
package email
package image
package validation
// Bad: A imports B, B imports A
package a
import "project/b"
package b
import "project/a" // Circular!
// Good: Extract shared interface to lower level
package contracts
type Processor interface { Process() }
package a
import "project/contracts"
package b
import "project/contracts"