5 min read
On this page

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 main package cannot be imported by other packages. Keep main thin — 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 main thin: wiring and startup only, with business logic in separate packages.