gary.info

here be dragons

Go Project Structure Reference

#go
go-projects.md

Go Project Structure Reference

Core Pattern (One Structure to Rule Them All)

myapp/
├── cmd/
│   └── {appname}/
│       └── main.go         # Entry point, minimal logic
├── internal/               # Private application code
│   ├── domain/            # Business entities & interfaces
│   │   ├── user.go        # type User struct
│   │   ├── errors.go      # Domain-specific errors
│   │   └── interfaces.go  # Repository interfaces
│   ├── service/           # Business logic
│   │   ├── user.go        # UserService
│   │   └── aggregate.go   # Multi-entity operations
│   ├── storage/           # Repository implementations
│   │   ├── postgres/
│   │   ├── memory/
│   │   └── cache/
│   └── transport/         # API layer
│       ├── http/
│       │   ├── handler.go
│       │   ├── middleware.go
│       │   └── routes.go
│       └── grpc/
│           └── server.go
├── pkg/                   # Public packages (importable)
│   ├── errors/
│   └── types/
├── migrations/            # Database migrations
│   └── 001_initial.sql
├── config/               # Configuration files
├── go.mod
├── go.sum
├── Makefile
└── README.md

Package Responsibilities

/cmd

  • Entry points only
  • Parse flags/env
  • Initialize dependencies
  • Start servers
  • /internal/domain

    go
    // Business entities
    type User struct {
        ID    string
        Email string
    }
    // Repository interfaces (ports)
    type UserRepository interface {
        Get(ctx context.Context, id string) (*User, error)
        Save(ctx context.Context, user *User) error
    }

    /internal/service

    go
    // Business logic
    type UserService struct {
        repo UserRepository
    }
    func (s UserService) CreateUser(ctx context.Context, email string) (User, error) {
        // Business rules here
    }

    /internal/storage

    go
    // Repository implementations (adapters)
    type PostgresUserRepo struct {
        db *sql.DB
    }
    func (r PostgresUserRepo) Get(ctx context.Context, id string) (User, error) {
        // SQL implementation
    }

    /internal/transport

    go
    // HTTP handlers
    func (h Handler) CreateUser(w http.ResponseWriter, r http.Request) {
        // HTTP concerns only
        // Call service layer
    }

    Scaling Patterns

    Single Module → Multiple Modules

    internal/
    ├── auth/                  # Module 1
    │   ├── domain/
    │   ├── service/
    │   ├── storage/
    │   └── transport/
    ├── billing/               # Module 2
    │   ├── domain/
    │   ├── service/
    │   ├── storage/
    │   └── transport/
    └── shared/               # Cross-module
        ├── middleware/
        └── database/

    CLI Applications

    mycli/
    ├── cmd/
    │   ├── root.go           # Command setup
    │   ├── init.go          # subcommand: mycli init
    │   └── sync.go          # subcommand: mycli sync
    ├── internal/
    │   └── core/            # Business logic
    ├── main.go              # Entry point
    └── go.mod

    Key Principles

    1. Dependency Rule

    transport → service → domain ← storage
             ↖__↗
  • Domain knows nothing about other layers
  • Service orchestrates domain logic
  • Transport/Storage are adapters
  • 2. Interface Ownership

  • Interfaces defined where they're used
  • Domain defines repository interfaces
  • Service defines external service interfaces
  • 3. Error Handling

    go
    // Domain errors
    var ErrUserNotFound = errors.New("user not found")
    // Wrap with context
    return fmt.Errorf("get user: %w", ErrUserNotFound)

    4. Configuration

    go
    type Config struct {
        Port     string env:"PORT" default:"8080"
        Database string env:"DATABASE_URL" required:"true"
    }

    Common Patterns

    Repository Pattern

    go
    type Repository[T any] interface {
        Get(ctx context.Context, id string) (T, error)
        List(ctx context.Context, filter Filter) ([]T, error)
        Save(ctx context.Context, entity T) error
        Delete(ctx context.Context, id string) error
    }

    Service Aggregates

    go
    type UserGroupAggregate struct {
        users  Repository[*User]
        groups Repository[*Group]
        members Repository[*Member]
    }

    Middleware Chain

    go
    type MiddlewareFunc func(http.Handler) http.Handler
    handler = middleware.Chain(
        middleware.Logger,
        middleware.Auth,
        middleware.RateLimit,
    )(handler)

    File Naming Conventions

  • user.go - Domain entity
  • user_service.go - Service layer
  • user_handler.go - HTTP handlers
  • user_test.go - Tests
  • interfaces.go - Shared interfaces
  • errors.go - Error definitions
  • Testing Structure

    internal/service/
    ├── user.go
    ├── user_test.go         # Unit tests
    └── userintegrationtest.go  # Integration tests

    When to Add Structure

  • Start with: domain/, transport/http/
  • Add service/ when: Business logic appears
  • Add storage/ when: Moving from in-memory
  • Add modules when: Clear bounded contexts emerge
  • Split to microservices when: Teams need independence
  • Quick Start Commands

    bash
    

    Initialize module

    go mod init github.com/user/myapp

    Create structure

    mkdir -p cmd/api internal/{domain,service,storage,transport/http}

    Run

    go run cmd/api/main.go

    Test

    go test ./...

    Build

    go build -o bin/api cmd/api/main.go

    Anti-Patterns to Avoid

    ❌ Deep nesting: internal/app/services/user/handlers/http/v1/ ❌ Circular dependencies between packages ❌ Business logic in handlers or repositories ❌ Shared mutable state ❌ God structs with too many dependencies

    Remember

  • Start simple: Add structure as needed
  • Interfaces over concretions: Define contracts
  • One package, one purpose: Clear responsibilities
  • Test the behavior: Not the implementation
  • Flat is better than nested: Avoid deep hierarchies

Based on DDD principles and modern Go practices. One structure that scales from prototype to production.