1. go
  2. /best practices

Best Practices for Go Development

This guide covers essential best practices for developing Go applications, helping you write clean, efficient, and maintainable code.

Project Structure

Standard Project Layout

├── cmd/
│   └── myapp/
│       └── main.go
├── internal/
│   ├── app/
│   │   └── app.go
│   ├── pkg1/
│   │   └── pkg1.go
│   └── pkg2/
│       └── pkg2.go
├── pkg/
│   └── shared/
│       └── shared.go
├── api/
│   └── openapi.yaml
├── web/
│   ├── templates/
│   └── static/
├── configs/
│   └── config.yaml
├── test/
│   └── integration/
├── scripts/
│   └── build.sh
├── docs/
│   └── README.md
├── go.mod
└── go.sum

Package Organization

// internal/app/app.go
package app

type App struct {
    config Config
    logger Logger
    db     Database
}

func New(config Config) *App {
    return &App{
        config: config,
        logger: newLogger(config),
        db:     newDatabase(config),
    }
}

Error Handling

Error Types

type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}

// Usage
if err := findUser(id); err != nil {
    var nfe *NotFoundError
    if errors.As(err, &nfe) {
        // Handle not found case
        return
    }
    // Handle other errors
}

Error Wrapping

func processUser(id string) error {
    user, err := findUser(id)
    if err != nil {
        return fmt.Errorf("finding user %s: %w", id, err)
    }
    
    if err := validateUser(user); err != nil {
        return fmt.Errorf("validating user %s: %w", id, err)
    }
    
    return nil
}

Logging

Structured Logging

import "go.uber.org/zap"

func initLogger() *zap.Logger {
    config := zap.NewProductionConfig()
    config.OutputPaths = []string{"stdout", "app.log"}
    logger, _ := config.Build()
    return logger
}

func processRequest(logger *zap.Logger, req *Request) {
    logger.Info("processing request",
        zap.String("request_id", req.ID),
        zap.String("user", req.UserID),
        zap.Duration("latency", req.Duration),
    )
}

Configuration

Using Configuration Management

type Config struct {
    Server struct {
        Host string `yaml:"host"`
        Port int    `yaml:"port"`
    } `yaml:"server"`
    Database struct {
        DSN string `yaml:"dsn"`
    } `yaml:"database"`
    Logger struct {
        Level string `yaml:"level"`
    } `yaml:"logger"`
}

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("reading config file: %w", err)
    }
    
    var config Config
    if err := yaml.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("parsing config: %w", err)
    }
    
    return &config, nil
}

Performance

Profiling

import "runtime/pprof"

func profile() {
    f, _ := os.Create("cpu.prof")
    defer f.Close()
    
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    
    // Code to profile
}

Memory Management

// Good: Reuse slices
func processItems(items []Item) {
    // Preallocate slice with known capacity
    results := make([]Result, 0, len(items))
    for _, item := range items {
        results = append(results, process(item))
    }
}

// Bad: Growing slice without preallocation
func processItemsBad(items []Item) {
    var results []Result // Will need to grow multiple times
    for _, item := range items {
        results = append(results, process(item))
    }
}

Security

Input Validation

func validateInput(input string) error {
    if len(input) > 100 {
        return errors.New("input too long")
    }
    
    if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(input) {
        return errors.New("input contains invalid characters")
    }
    
    return nil
}

Secure Configuration

func loadSecrets() error {
    // Use environment variables for sensitive data
    dbPassword := os.Getenv("DB_PASSWORD")
    if dbPassword == "" {
        return errors.New("DB_PASSWORD environment variable not set")
    }
    
    // Never log sensitive information
    log.Info("database connection established")
    
    return nil
}

Code Style

Consistent Formatting

// Use gofmt
// Run: gofmt -w .

// Use consistent naming
type User struct {
    ID        string    // Use ID, not Id
    CreatedAt time.Time // Use CreatedAt, not Created_At
}

// Use meaningful variable names
func processTransaction(tx *Transaction) error {
    // Good: Clear and descriptive
    userID := tx.UserID
    amount := tx.Amount
    
    // Bad: Unclear abbreviations
    uid := tx.UserID
    amt := tx.Amount
}

Documentation

// Package users provides functionality for user management.
package users

// User represents a system user with associated metadata.
type User struct {
    ID      string
    Name    string
    Email   string
    Created time.Time
}

// FindByID retrieves a user by their unique identifier.
// Returns ErrNotFound if the user doesn't exist.
func FindByID(id string) (*User, error) {
    // Implementation
}

Testing

Table-Driven Tests

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {
            name:    "valid email",
            email:   "[email protected]",
            wantErr: false,
        },
        {
            name:    "invalid email",
            email:   "invalid-email",
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("validateEmail() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Next Steps