Go Project Structure: Practices & Patterns
Structure your Go projects for scalability and clarity
Structuring a Go project effectively is fundamental to long-term maintainability, team collaboration, and scalability. Unlike frameworks that enforce rigid directory layouts, Go embraces flexibility—but with that freedom comes the responsibility to choose patterns that serve your project’s specific needs.

Understanding Go’s Philosophy on Project Structure
Go’s minimalist design philosophy extends to project organization. The language doesn’t mandate a specific structure, instead trusting developers to make informed decisions. This approach has led the community to develop several proven patterns, from simple flat layouts for small projects to sophisticated architectures for enterprise systems.
The key principle is simplicity first, complexity when necessary. Many developers fall into the trap of over-engineering their initial structure, creating deeply nested directories and premature abstractions. Start with what you need today, and refactor as your project grows.
When Should I Use the internal/ Directory versus pkg/?
The internal/ directory serves a specific purpose in Go’s design: it contains packages that cannot be imported by external projects. Go’s compiler enforces this restriction, making internal/ perfect for private application logic, business rules, and utilities not meant for reuse outside your project.
The pkg/ directory, on the other hand, signals that code is intended for external consumption. Only place code here if you’re building a library or reusable components that you want others to import. Many projects don’t need pkg/ at all—if your code isn’t being consumed externally, keeping it at the root or in internal/ is cleaner. When building reusable libraries, consider leveraging Go generics to create type-safe, reusable components.
The Standard Go Project Layout
The most widely recognized pattern is the golang-standards/project-layout, though it’s important to note this isn’t an official standard. Here’s what a typical structure looks like:
myproject/
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/
│ ├── auth/
│ ├── storage/
│ └── transport/
├── pkg/
│ ├── logger/
│ └── crypto/
├── api/
│ └── openapi.yaml
├── config/
│ └── config.yaml
├── scripts/
│ └── deploy.sh
├── go.mod
├── go.sum
└── README.md
The cmd/ Directory
The cmd/ directory contains your application entry points. Each subdirectory represents a separate executable binary. For example, cmd/api/main.go builds your API server, while cmd/worker/main.go might build a background job processor.
Best practice: Keep your main.go files minimal—just enough to wire up dependencies, load configuration, and start the application. All substantive logic belongs in packages that main.go imports.
// cmd/api/main.go
package main
import (
"log"
"myproject/internal/server"
"myproject/internal/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
srv := server.New(cfg)
if err := srv.Start(); err != nil {
log.Fatal(err)
}
}
The internal/ Directory
This is where your private application code lives. The Go compiler prevents any external project from importing packages within internal/, making it ideal for:
- Business logic and domain models
- Application services
- Internal APIs and interfaces
- Database repositories (for choosing the right ORM, see our comparison of Go ORMs for PostgreSQL)
- Authentication and authorization logic
Organize internal/ by feature or domain, not by technical layer. Instead of internal/handlers/, internal/services/, internal/repositories/, prefer internal/user/, internal/order/, internal/payment/ where each package contains its handlers, services, and repositories.
Should I Start with a Complex Directory Structure?
Absolutely not. If you’re building a small tool or prototype, start with:
myproject/
├── main.go
├── go.mod
└── go.sum
As your project grows and you identify logical groupings, introduce directories. You might add a db/ package when database logic becomes substantial, or an api/ package when HTTP handlers multiply. Let the structure emerge naturally rather than imposing it upfront.
Flat vs. Nested Structures: Finding Balance
One of the most common mistakes in Go project structure is excessive nesting. Go favors shallow hierarchies—typically one or two levels deep. Deep nesting increases cognitive load and makes imports cumbersome.
What Are the Most Common Mistakes?
Over-nesting directories: Avoid structures like internal/services/user/handlers/http/v1/. This creates unnecessary navigation complexity. Instead, use internal/user/handler.go.
Generic package names: Names like utils, helpers, common, or base are code smells. They don’t convey specific functionality and often become dumping grounds for unrelated code. Use descriptive names: validator, auth, storage, cache.
Circular dependencies: When package A imports package B, and B imports A, you have a circular dependency—a compilation error in Go. This typically signals poor separation of concerns. Introduce interfaces or extract shared types into a separate package.
Mixing concerns: Keep HTTP handlers focused on HTTP concerns, database repositories on data access, and business logic in service packages. Placing business rules in handlers makes testing difficult and couples your domain logic to HTTP.
Domain-Driven Design and Hexagonal Architecture
For larger applications, especially microservices, Domain-Driven Design (DDD) with Hexagonal Architecture provides clear separation of concerns.
How Do I Structure a Microservice Following Domain-Driven Design?
Hexagonal Architecture organizes code in concentric layers with dependencies flowing inward:
internal/
├── domain/
│ └── user/
│ ├── entity.go # Domain models and value objects
│ ├── repository.go # Repository interface (port)
│ └── service.go # Domain services
├── application/
│ └── user/
│ ├── create_user.go # Use case: create user
│ ├── get_user.go # Use case: retrieve user
│ └── service.go # Application service orchestration
├── adapter/
│ ├── http/
│ │ └── user_handler.go # HTTP adapter (REST endpoints)
│ ├── postgres/
│ │ └── user_repo.go # Database adapter (implements repository port)
│ └── redis/
│ └── cache.go # Cache adapter
└── api/
└── http/
└── router.go # Route configuration and middleware
Domain layer (domain/): Core business logic, entities, value objects, and domain service interfaces. This layer has no dependencies on external systems—no HTTP, no database imports. It defines repository interfaces (ports) that adapters implement.
Application layer (application/): Use cases that orchestrate domain objects. Each use case (e.g., “create user”, “process payment”) is a separate file or package. This layer coordinates domain objects but contains no business rules itself.
Adapter layer (adapter/): Implements interfaces defined by inner layers. HTTP handlers convert requests to domain objects, database repositories implement persistence, message queues handle async communication. This layer contains all framework-specific and infrastructure code.
API layer (api/): Routes, middleware, DTOs (Data Transfer Objects), API versioning, and OpenAPI specifications.
This structure ensures your core business logic remains testable and independent of frameworks, databases, or external services. You can swap PostgreSQL for MongoDB, or REST for gRPC, without touching domain code.
Practical Patterns for Different Project Types
Small CLI Tool
For command-line applications, you’ll want a structure that supports multiple commands and subcommands. Consider using frameworks like Cobra for command structure and Viper for configuration management. Our guide on building CLI applications in Go with Cobra & Viper covers this pattern in detail.
mytool/
├── main.go
├── command/
│ ├── root.go
│ └── version.go
├── go.mod
└── README.md
REST API Service
When building REST APIs in Go, this structure separates concerns cleanly: handlers handle HTTP concerns, services contain business logic, and repositories manage data access. For a comprehensive guide covering standard library approaches, frameworks, authentication, testing patterns, and production-ready best practices, see our complete guide to building REST APIs in Go.
myapi/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── config/
│ ├── middleware/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ └── repository.go
│ └── product/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
├── pkg/
│ └── httputil/
├── go.mod
└── README.md
Monorepo with Multiple Services
myproject/
├── cmd/
│ ├── api/
│ ├── worker/
│ └── scheduler/
├── internal/
│ ├── shared/ # Shared internal packages
│ ├── api/ # API-specific code
│ ├── worker/ # Worker-specific code
│ └── scheduler/ # Scheduler-specific code
├── pkg/ # Shared libraries
├── go.work # Go workspace file
└── README.md
Testing and Documentation
Place test files alongside the code they test using the _test.go suffix:
internal/
└── user/
├── service.go
├── service_test.go
├── repository.go
└── repository_test.go
This convention keeps tests close to implementation, making them easy to find and maintain. For integration tests that touch multiple packages, create a separate test/ directory at the project root. For comprehensive guidance on writing effective unit tests, including table-driven tests, mocks, coverage analysis, and best practices, see our guide to Go unit testing structure and best practices.
Documentation belongs in:
README.md: Project overview, setup instructions, basic usagedocs/: Detailed documentation, architecture decisions, API referencesapi/: OpenAPI/Swagger specifications, protobuf definitions
For REST APIs, generating and serving OpenAPI documentation with Swagger is essential for API discoverability and developer experience. Our guide on adding Swagger to your Go API covers integration with popular frameworks and best practices.
Managing Dependencies with Go Modules
Every Go project should use Go Modules for dependency management. For a comprehensive reference on Go commands and module management, check our Go Cheatsheet. Initialize with:
go mod init github.com/yourusername/myproject
This creates go.mod (dependencies and versions) and go.sum (checksums for verification). Keep these files in version control for reproducible builds.
Update dependencies regularly:
go get -u ./... # Update all dependencies
go mod tidy # Remove unused dependencies
go mod verify # Verify checksums
Key Takeaways
-
Start simple, evolve naturally: Don’t over-engineer your initial structure. Add directories and packages when complexity demands it.
-
Favor flat hierarchies: Limit nesting to one or two levels. Go’s flat package structure improves readability.
-
Use descriptive package names: Avoid generic names like
utils. Name packages after what they do:auth,storage,validator. -
Separate concerns clearly: Keep handlers focused on HTTP, repositories on data access, and business logic in service packages.
-
Leverage
internal/for privacy: Use it for code that shouldn’t be imported externally. Most application code belongs here. -
Apply architecture patterns when needed: For complex systems, Hexagonal Architecture and DDD provide clear boundaries and testability.
-
Let Go guide you: Follow Go idioms rather than importing patterns from other languages. Go has its own philosophy on simplicity and organization.
Useful links
- Go Cheatsheet
- Building REST APIs in Go: Complete Guide
- Building CLI Applications in Go with Cobra & Viper
- Go Unit Testing: Structure & Best Practices
- Adding Swagger to Your Go API
- Go Generics: Use Cases and Patterns
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
Other Related Articles
- Standard Go Project Layout - Community-driven project structure guidelines
- Go Modules Reference - Official Go modules documentation
- Hexagonal Architecture in Go - Enterprise-grade hexagonal architecture framework
- Domain-Driven Design in Go - Production-ready DDD implementation
- Go Project Structure Conventions - Additional patterns and examples
- Effective Go - Official Go best practices guide