Go doesn’t enforce a project layout. That freedom is powerful but requires discipline — start simple and let structure emerge as complexity grows.
Start Simple
For a small tool or prototype, a flat layout is perfectly fine:
mytool/
├── main.go
├── go.mod
└── go.sum
Don’t create directories you don’t need yet. Add structure when the code tells you to — when files multiply, when concerns overlap, or when you need multiple binaries.
The Standard Layout
As a project grows, the community-established layout provides a solid foundation:
myproject/
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/
│ ├── auth/
│ ├── storage/
│ └── transport/
├── pkg/
│ └── httputil/
├── go.mod
├── go.sum
└── README.md
This isn’t an official Go standard — it’s a widely adopted convention from golang-standards/project-layout. The three key directories each serve a distinct purpose.
The cmd/ Directory
Each subdirectory under cmd/ is a separate binary. Keep main.go minimal — just wire dependencies, load config, and start the application:
// cmd/api/main.go
package main
import (
"log"
"myproject/internal/config"
"myproject/internal/server"
)
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)
}
}
All substantive logic belongs in packages that main.go imports, not in main.go itself.
The internal/ Directory
The Go compiler enforces that packages inside internal/ cannot be imported by external projects. This makes it the right place for:
- Business logic and domain models
- Application services and use cases
- Database repositories
- Authentication and authorization
Organize by domain, not by technical layer. Instead of internal/handlers/, internal/services/, internal/repositories/, prefer:
internal/
├── user/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
├── order/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
└── config/
└── config.go
Each package owns its full vertical slice — handlers, services, and data access together. This reduces cross-package dependencies and keeps related code close.
The pkg/ Directory
pkg/ signals that code is intended for external consumption. Only use it if you’re building a library or reusable components that other projects should import.
Most applications don’t need pkg/ at all. If your code isn’t consumed externally, keep it in internal/ or at the project root. Don’t create pkg/ just because you saw it in another project.
Hexagonal Architecture
For larger services, hexagonal architecture (ports and adapters) provides clear separation with dependencies flowing inward:
internal/
├── domain/
│ └── user/
│ ├── entity.go # Domain models, value objects
│ ├── repository.go # Port: repository interface
│ └── service.go # Domain logic
├── application/
│ └── user/
│ ├── create.go # Use case: create user
│ └── get.go # Use case: get user
└── adapter/
├── http/
│ └── user_handler.go # Adapter: REST endpoints
├── postgres/
│ └── user_repo.go # Adapter: implements repository port
└── redis/
└── cache.go # Adapter: caching layer
The domain layer has zero external dependencies — no HTTP, no database imports. It defines interfaces (ports) that the adapter layer implements.
The application layer orchestrates domain objects into use cases but contains no business rules itself.
The adapter layer plugs into the outside world: HTTP handlers, database implementations, message queues. You can swap PostgreSQL for MongoDB or REST for gRPC without touching domain code.
This pattern is overkill for small projects but pays off when your service grows complex or needs to support multiple transports.
Common Mistakes
Over-nesting directories. Avoid internal/services/user/handlers/http/v1/. Go favors shallow hierarchies — one or two levels deep. Use internal/user/handler.go instead.
Generic package names. Names like utils, helpers, common, or base are code smells. They become dumping grounds for unrelated code. Use descriptive names: validator, auth, cache.
Circular dependencies. When package A imports B and B imports A, the compiler rejects it. This signals poor separation of concerns. Fix it by extracting shared types into a third package or introducing interfaces.
Business logic in handlers. Keep HTTP handlers focused on HTTP concerns — parsing requests, writing responses. Business rules belong in service packages. This makes testing easier and decouples domain logic from transport.
Practical Layouts
CLI Tool
mytool/
├── main.go
├── cmd/
│ ├── root.go
│ ├── serve.go
│ └── version.go
├── go.mod
└── README.md
REST API
myapi/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── config/
│ ├── middleware/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ └── repository.go
│ └── product/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
├── go.mod
└── README.md
Monorepo with Multiple Services
myproject/
├── cmd/
│ ├── api/
│ ├── worker/
│ └── scheduler/
├── internal/
│ ├── shared/
│ ├── api/
│ ├── worker/
│ └── scheduler/
├── go.work
├── go.mod
└── README.md
Use go.work to manage a workspace when services share internal packages but need independent module paths.
Testing Conventions
Place test files alongside the code they test:
internal/
└── user/
├── service.go
├── service_test.go
├── repository.go
└── repository_test.go
For integration tests that span multiple packages, add a test/ directory at the project root.
Conclusion
Start flat, add structure when the code demands it, and organize by domain rather than technical layer. The best project layout is the simplest one that keeps your team productive — don’t over-engineer it upfront.