Project Structure
Go does not enforce a single project layout. There is no framework that dictates where files go. This flexibility is a strength, but it means teams must make deliberate choices. The right structure depends on the project's size and complexity. Start flat, add structure as the project grows.
Flat Structure for Small Projects
For a small tool, service, or library, a flat layout is the best starting point. All .go files sit in the root directory.
mylib/
go.mod
go.sum
mylib.go
mylib_test.go
helpers.go
Or for a small application:
mytool/
go.mod
go.sum
main.go
config.go
handler.go
handler_test.go
This is how many standard library packages are organized. There is nothing wrong with a flat structure — it is simple, easy to navigate, and has no import path complexity. Do not add directories until you have a reason.
cmd/ for Multiple Binaries
When your project produces more than one executable, use a cmd/ directory with a subdirectory for each binary.
myproject/
go.mod
go.sum
cmd/
api/
main.go
worker/
main.go
migrate/
main.go
server.go
server_test.go
store.go
store_test.go
Each subdirectory under cmd/ contains a main.go with package main. The binary name matches the directory name by default.
// cmd/api/main.go
package main
import (
"fmt"
"myproject"
)
func main() {
srv := myproject.NewServer(myproject.DefaultConfig())
fmt.Println("Starting API server...")
srv.Start()
}
Build them individually:
go build ./cmd/api
go build ./cmd/worker
go build ./cmd/migrate
The cmd/ directories should be thin — just wiring and startup. Business logic lives in the root package or in sub-packages.
internal/ for Private Packages
The internal/ directory is enforced by the Go toolchain. Code inside internal/ can only be imported by code within the parent directory tree.
myproject/
go.mod
cmd/
api/
main.go
internal/
auth/
auth.go
auth_test.go
database/
postgres.go
postgres_test.go
middleware/
logging.go
server.go
Code in cmd/api/ and server.go can import myproject/internal/auth. Code in a different module cannot. This gives you sub-packages for organization without committing to a public API.
// cmd/api/main.go
package main
import (
"myproject/internal/auth"
"myproject/internal/database"
)
func main() {
db := database.Connect("postgres://localhost/mydb")
authenticator := auth.New(db)
// ...
}
No pkg/ (Usually Unnecessary)
You will see some projects with a pkg/ directory meant to hold "importable library code." This convention is controversial in the Go community and usually unnecessary.
# Common but often unnecessary
myproject/
pkg/
auth/
store/
cmd/
api/
# Simpler alternative
myproject/
auth/
store/
cmd/
api/
The pkg/ directory adds a path segment without adding meaning. If your code is importable, it is already clear from the directory name. If you need to distinguish between public and private packages, use internal/ for the private ones.
Some large projects (Kubernetes, for example) use pkg/ because of their scale and history. For most projects, it is an extra directory that does not earn its keep.
The Standard Project Layout Debate
The "golang-standards/project-layout" repository on GitHub is frequently referenced but also frequently criticized. It is not an official Go standard. The Go team has explicitly said it does not endorse it.
What matters more than following a prescriptive layout is:
- Consistency within your team. Pick a structure and stick with it.
- Starting simple. Do not create directories you do not need yet.
- Growing organically. Refactor the structure as the project's needs become clear.
A project that starts with twenty empty directories is harder to navigate than one that starts flat and adds structure when the code demands it.
A Real-World Project Structure
Here is a structure for a mid-sized web service, the kind that handles HTTP requests, talks to a database, and runs background jobs.
orderservice/
go.mod
go.sum
cmd/
api/
main.go # HTTP server startup
worker/
main.go # Background job runner
internal/
order/
order.go # Order type, business logic
order_test.go
store.go # OrderStore interface
postgres.go # PostgreSQL implementation
postgres_test.go
user/
user.go
user_test.go
store.go
postgres.go
notification/
email.go
email_test.go
http/
handler.go # HTTP handlers
handler_test.go
middleware.go
routes.go
config/
config.go # Configuration loading
migrations/
001_create_users.sql
002_create_orders.sql
testdata/
fixtures.json
What Each Directory Does
cmd/ holds the entry points. Each binary has its own directory with a thin main.go that wires everything together.
// cmd/api/main.go
package main
import (
"log"
"net/http"
"orderservice/config"
httphandler "orderservice/http"
"orderservice/internal/order"
)
func main() {
cfg := config.Load()
orderStore, err := order.NewPostgresStore(cfg.DatabaseURL)
if err != nil {
log.Fatal(err)
}
handler := httphandler.NewHandler(orderStore)
router := httphandler.NewRouter(handler)
log.Printf("listening on %s", cfg.Addr)
log.Fatal(http.ListenAndServe(cfg.Addr, router))
}
internal/ holds business logic. Each domain concept gets its own package. The order package defines the Order type, the OrderStore interface, and the PostgreSQL implementation.
http/ holds the HTTP layer. It imports from internal/ but is itself importable by other packages if needed. If you want to hide it, move it under internal/.
config/ loads configuration from environment variables or files.
migrations/ holds database migration files. Not Go code, but part of the project.
testdata/ holds test fixtures. The Go toolchain ignores directories named testdata/ during builds.
Growing the Structure
As the project grows, you might add:
orderservice/
...
internal/
...
platform/
postgres/ # shared database utilities
redis/ # shared cache utilities
queue/
publisher.go
consumer.go
docs/
api.yaml # OpenAPI spec
scripts/
seed.sh
deploy.sh
Each addition should be motivated by a real need, not by a template. If you find yourself creating a utils/ package, stop and think about where those utilities actually belong. They usually fit better in the package that uses them.
Common Pitfalls
- Over-structuring from the start. Creating directories for future code that may never arrive. Start flat and extract packages when you feel the pain of a single package growing too large.
- Circular dependencies. Go does not allow circular imports. If package A imports B and B needs A, extract the shared types into a third package that both can import.
- Too many tiny packages. A package with one file and one type creates import overhead without benefit. Group related types together.
- Putting everything in main. The
mainpackage cannot be imported by other packages. Keepmainthin — it should only handle wiring and startup. - Naming packages after patterns. Avoid names like
models/,controllers/,services/. Name packages after what they contain:user/,order/,notification/.
Key Takeaways
- Start with a flat structure. Add directories only when the code demands it.
- Use
cmd/when your project produces multiple binaries. - Use
internal/to create packages that are private to your module. - Skip
pkg/unless you have a strong reason — it usually adds noise. - There is no official Go project layout standard. Consistency within your team matters more than any template.
- Name packages after domain concepts, not architectural patterns.
- Keep
mainthin: wiring and startup only, with business logic in separate packages.