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