Go Development Patterns
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
When to Activate
-
Writing new Go code
-
Reviewing Go code
-
Refactoring existing Go code
-
Designing Go packages/modules
Core Principles
- Simplicity and Clarity
Go favors simplicity over cleverness. Code should be obvious and easy to read.
// Good: Clear and direct func GetUser(id string) (*User, error) { user, err := db.FindUser(id) if err != nil { return nil, fmt.Errorf("get user %s: %w", id, err) } return user, nil }
// Bad: Overly clever func GetUser(id string) (*User, error) { return func() (*User, error) { if u, e := db.FindUser(id); e == nil { return u, nil } else { return nil, e } }() }
- Make the Zero Value Useful
Design types so their zero value is immediately usable without initialization.
// Good: Zero value is useful type Counter struct { mu sync.Mutex count int // zero value is 0, ready to use }
func (c *Counter) Inc() { c.mu.Lock() c.count++ c.mu.Unlock() }
// Good: bytes.Buffer works with zero value var buf bytes.Buffer buf.WriteString("hello")
// Bad: Requires initialization type BadCounter struct { counts map[string]int // nil map will panic }
- Accept Interfaces, Return Structs
Functions should accept interface parameters and return concrete types.
// Good: Accepts interface, returns concrete type func ProcessData(r io.Reader) (*Result, error) { data, err := io.ReadAll(r) if err != nil { return nil, err } return &Result{Data: data}, nil }
// Bad: Returns interface (hides implementation details unnecessarily) func ProcessData(r io.Reader) (io.Reader, error) { // ... }
Error Handling Patterns
Error Wrapping with Context
// Good: Wrap errors with context func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("load config %s: %w", path, err) }
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
Custom Error Types
// Define domain-specific errors type ValidationError struct { Field string Message string }
func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) }
// Sentinel errors for common cases var ( ErrNotFound = errors.New("resource not found") ErrUnauthorized = errors.New("unauthorized") ErrInvalidInput = errors.New("invalid input") )
Error Checking with errors.Is and errors.As
func HandleError(err error) { // Check for specific error if errors.Is(err, sql.ErrNoRows) { log.Println("No records found") return }
// Check for error type
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
// Unknown error
log.Printf("Unexpected error: %v", err)
}
Never Ignore Errors
// Bad: Ignoring error with blank identifier result, _ := doSomething()
// Good: Handle or explicitly document why it's safe to ignore result, err := doSomething() if err != nil { return err }
// Acceptable: When error truly doesn't matter (rare) _ = writer.Close() // Best-effort cleanup, error logged elsewhere
Concurrency Patterns
Worker Pool
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
Context for Cancellation and Timeouts
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Graceful Shutdown
func GracefulShutdown(server *http.Server) { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
errgroup for Coordinated Goroutines
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) { g, ctx := errgroup.WithContext(ctx) results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // Capture loop variables
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Avoiding Goroutine Leaks
// Bad: Goroutine leak if context is cancelled func leakyFetch(ctx context.Context, url string) <-chan []byte { ch := make(chan []byte) go func() { data, _ := fetch(url) ch <- data // Blocks forever if no receiver }() return ch }
// Good: Properly handles cancellation func safeFetch(ctx context.Context, url string) <-chan []byte { ch := make(chan []byte, 1) // Buffered channel go func() { data, err := fetch(url) if err != nil { return } select { case ch <- data: case <-ctx.Done(): } }() return ch }
Interface Design
Small, Focused Interfaces
// Good: Single-method interfaces type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }
// Compose interfaces as needed type ReadWriteCloser interface { Reader Writer Closer }
Define Interfaces Where They're Used
// In the consumer package, not the provider package service
// UserStore defines what this service needs type UserStore interface { GetUser(id string) (*User, error) SaveUser(user *User) error }
type Service struct { store UserStore }
// Concrete implementation can be in another package // It doesn't need to know about this interface
Optional Behavior with Type Assertions
type Flusher interface { Flush() error }
func WriteAndFlush(w io.Writer, data []byte) error { if _, err := w.Write(data); err != nil { return err }
// Flush if supported
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
Package Organization
Standard Project Layout
myproject/ ├── cmd/ │ └── myapp/ │ └── main.go # Entry point ├── internal/ │ ├── handler/ # HTTP handlers │ ├── service/ # Business logic │ ├── repository/ # Data access │ └── config/ # Configuration ├── pkg/ │ └── client/ # Public API client ├── api/ │ └── v1/ # API definitions (proto, OpenAPI) ├── testdata/ # Test fixtures ├── go.mod ├── go.sum └── Makefile
Package Naming
// Good: Short, lowercase, no underscores package http package json package user
// Bad: Verbose, mixed case, or redundant package httpHandler package json_parser package userService // Redundant 'Service' suffix
Avoid Package-Level State
// Bad: Global mutable state var db *sql.DB
func init() { db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) }
// Good: Dependency injection type Server struct { db *sql.DB }
func NewServer(db *sql.DB) *Server { return &Server{db: db} }
Struct Design
Functional Options Pattern
type Server struct { addr string timeout time.Duration logger *log.Logger }
type Option func(*Server)
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithLogger(l *log.Logger) Option { return func(s *Server) { s.logger = l } }
func NewServer(addr string, opts ...Option) *Server { s := &Server{ addr: addr, timeout: 30 * time.Second, // default logger: log.Default(), // default } for _, opt := range opts { opt(s) } return s }
// Usage server := NewServer(":8080", WithTimeout(60*time.Second), WithLogger(customLogger), )
Embedding for Composition
type Logger struct { prefix string }
func (l *Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.prefix, msg) }
type Server struct { *Logger // Embedding - Server gets Log method addr string }
func NewServer(addr string) *Server { return &Server{ Logger: &Logger{prefix: "SERVER"}, addr: addr, } }
// Usage s := NewServer(":8080") s.Log("Starting...") // Calls embedded Logger.Log
Memory and Performance
Preallocate Slices When Size is Known
// Bad: Grows slice multiple times func processItems(items []Item) []Result { var results []Result for _, item := range items { results = append(results, process(item)) } return results }
// Good: Single allocation func processItems(items []Item) []Result { results := make([]Result, 0, len(items)) for _, item := range items { results = append(results, process(item)) } return results }
Use sync.Pool for Frequent Allocations
var bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, }
func ProcessRequest(data []byte) []byte { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }()
buf.Write(data)
// Process...
return buf.Bytes()
}
Avoid String Concatenation in Loops
// Bad: Creates many string allocations func join(parts []string) string { var result string for _, p := range parts { result += p + "," } return result }
// Good: Single allocation with strings.Builder func join(parts []string) string { var sb strings.Builder for i, p := range parts { if i > 0 { sb.WriteString(",") } sb.WriteString(p) } return sb.String() }
// Best: Use standard library func join(parts []string) string { return strings.Join(parts, ",") }
Go Tooling Integration
Essential Commands
Build and run
go build ./... go run ./cmd/myapp
Testing
go test ./... go test -race ./... go test -cover ./...
Static analysis
go vet ./... staticcheck ./... golangci-lint run
Module management
go mod tidy go mod verify
Formatting
gofmt -w . goimports -w .
Recommended Linter Configuration (.golangci.yml)
linters: enable: - errcheck - gosimple - govet - ineffassign - staticcheck - unused - gofmt - goimports - misspell - unconvert - unparam
linters-settings: errcheck: check-type-assertions: true govet: check-shadowing: true
issues: exclude-use-default: false
Quick Reference: Go Idioms
Idiom Description
Accept interfaces, return structs Functions accept interface params, return concrete types
Errors are values Treat errors as first-class values, not exceptions
Don't communicate by sharing memory Use channels for coordination between goroutines
Make the zero value useful Types should work without explicit initialization
A little copying is better than a little dependency Avoid unnecessary external dependencies
Clear is better than clever Prioritize readability over cleverness
gofmt is no one's favorite but everyone's friend Always format with gofmt/goimports
Return early Handle errors first, keep happy path unindented
Anti-Patterns to Avoid
// Bad: Naked returns in long functions func process() (result int, err error) { // ... 50 lines ... return // What is being returned? }
// Bad: Using panic for control flow func GetUser(id string) *User { user, err := db.Find(id) if err != nil { panic(err) // Don't do this } return user }
// Bad: Passing context in struct type Request struct { ctx context.Context // Context should be first param ID string }
// Good: Context as first parameter func ProcessRequest(ctx context.Context, id string) error { // ... }
// Bad: Mixing value and pointer receivers type Counter struct{ n int } func (c Counter) Value() int { return c.n } // Value receiver func (c *Counter) Increment() { c.n++ } // Pointer receiver // Pick one style and be consistent
Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.