Featured image of post Go Project Layout: The Official Standards for Scalable Go Projects

Go Project Layout: The Official Standards for Scalable Go Projects

Set of common historical and emerging project layout patterns in the Go ecosystem. Note: despite the org-name they do not represent official golang standards, see [this issue](https://github.com/golang-standards/project-layout/issues/117) for more information. Nonetheless, some may find the layout useful.

Introduction

The golang-standards/project-layout repository is one of the most influential resources in the Go ecosystem, boasting 55,271 stars on GitHub (as of this writing). While not an official standard from the Go team, this community-maintained project documents widely adopted patterns for organizing Go projects at scale. It serves as a battle-tested template for structuring production-grade applications, libraries, and services.

For Go developers, project organization becomes critical when:

  1. Building applications that outgrow a single main.go file
  2. Collaborating with teams that need clear boundaries between components
  3. Creating reusable libraries consumed by other projects
  4. Implementing CI/CD pipelines that require standardized test/build locations

The layout solves common problems like:

  • Codebase discoverability: Where to find tests, configuration, or business logic
  • Dependency management: Separating internal and external packages
  • Build optimization: Organizing binaries, assets, and deployment artifacts
  • Testing consistency: Locating unit, integration, and end-to-end tests

Real-world adopters include infrastructure tools (Terraform providers), web services (REST APIs), and enterprise applications requiring clear separation between domains.

Key Features

  1. /cmd Directory

    • Contains main applications for your project
    • Each subdirectory represents a standalone binary (e.g., cmd/server, cmd/cli)
    • Prevents dependency collisions between multiple executables
  2. /internal Directory

    • Stores private application code not meant for external consumption
    • Enforced by Go’s compiler to prevent imports from outside your project
    • Ideal for business logic and domain-specific implementations
  3. /pkg Directory

    • Houses public code that others can import
    • Used when developing reusable libraries/components
    • Clearly distinguishes exported vs internal functionality
  4. Standardized Testing Locations

    • TestMain setup in /pkg or /internal packages
    • /test directory for integration/end-to-end tests
    • /api for Protobuf/OpenAPI specs and contract tests
  5. Build/Deployment Support

    • /build for CI/CD scripts and Dockerfiles
    • /configs for deployment configurations
    • /deployments for orchestration templates (Kubernetes, Terraform)

Compared to flat structures, this layout explicitly defines:

1project-root/
2├── cmd/
3├── internal/
4├── pkg/
5├── test/
6└── ...other standard dirs

Installation and Setup

This isn’t a traditional library—it’s a project template. To use it:

  1. Clone the repository as a reference:
1git clone https://github.com/golang-standards/project-layout.git
  1. Create a new project with the same structure:
1mkdir -p my-project/{cmd,internal,pkg,test}

Requirements:

  • Go 1.20+ (for modern module support)
  • No external dependencies required

Verify your structure matches core directories:

1tree -d -L 1 my-project
2# Should show: cmd, internal, pkg, test

Basic Usage

Minimal Web Server Example

Directory structure:

1my-app/
2├── cmd/
3│   └── server/
4│       └── main.go
5├── internal/
6│   └── handler/
7│       └── handler.go
8└── go.mod

internal/handler/handler.go:

 1package handler
 2
 3import (
 4    "fmt"
 5    "net/http"
 6)
 7
 8func Hello(w http.ResponseWriter, r *http.Request) {
 9    fmt.Fprint(w, "Hello from structured project!")
10}

cmd/server/main.go:

 1package main
 2
 3import (
 4    "log"
 5    "net/http"
 6    "my-app/internal/handler"
 7)
 8
 9func main() {
10    mux := http.NewServeMux()
11    mux.HandleFunc("/", handler.Hello)
12    
13    log.Println("Server running on :8080")
14    log.Fatal(http.ListenAndServe(":8080", mux))
15}

Run and test:

1go run cmd/server/main.go
2# curl localhost:8080 → "Hello from structured project!"

Real-World Examples

Example 1: Production REST API

Structure:

 1api/
 2├── cmd/
 3│   ├── api-server/
 4│   │   └── main.go
 5│   └── migrate/
 6│       └── main.go
 7├── internal/
 8│   ├── user/
 9│   │   ├── service.go
10│   │   └── handler.go
11│   └── middleware/
12│       └── auth.go
13├── pkg/
14│   └── database/
15│       └── postgres.go
16└── test/
17    └── integration/
18        └── user_test.go

pkg/database/postgres.go (simplified):

 1package database
 2
 3import (
 4    "context"
 5    "database/sql"
 6    _ "github.com/lib/pq"
 7)
 8
 9type DB struct {
10    *sql.DB
11}
12
13func New(ctx context.Context, connStr string) (*DB, error) {
14    db, err := sql.Open("postgres", connStr)
15    if err != nil {
16        return nil, err
17    }
18    
19    if err := db.PingContext(ctx); err != nil {
20        return nil, err
21    }
22    
23    return &DB{db}, nil
24}

internal/user/handler.go:

 1package user
 2
 3import (
 4    "encoding/json"
 5    "net/http"
 6)
 7
 8type Service interface {
 9    GetUser(id string) (*User, error)
10}
11
12type Handler struct {
13    svc Service
14}
15
16func NewHandler(svc Service) *Handler {
17    return &Handler{svc: svc}
18}
19
20func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
21    user, err := h.svc.GetUser(r.PathValue("id"))
22    if err != nil {
23        http.Error(w, err.Error(), http.StatusNotFound)
24        return
25    }
26    
27    json.NewEncoder(w).Encode(user)
28}

cmd/api-server/main.go:

 1package main
 2
 3import (
 4    "context"
 5    "log"
 6    "net/http"
 7    
 8    "my-api/internal/user"
 9    "my-api/internal/middleware"
10    "my-api/pkg/database"
11)
12
13func main() {
14    ctx := context.Background()
15    
16    // Initialize dependencies
17    db, err := database.New(ctx, "postgres://...")
18    if err != nil {
19        log.Fatal(err)
20    }
21    defer db.Close()
22    
23    userSvc := user.NewService(db)
24    userHandler := user.NewHandler(userSvc)
25    
26    // Setup routes
27    mux := http.NewServeMux()
28    mux.HandleFunc("GET /users/{id}", userHandler.GetUser)
29    
30    // Add middleware
31    wrappedMux := middleware.Auth(mux)
32    
33    log.Println("API server starting on :8080")
34    log.Fatal(http.ListenAndServe(":8080", wrappedMux))
35}

Example 2: CLI Tool with Subcommands

Structure:

 1cli-tool/
 2├── cmd/
 3   ├── cli/
 4      └── main.go
 5├── internal/
 6   └── command/
 7       ├── root.go
 8       ├── generate.go
 9       └── validate.go
10└── pkg/
11    └── utils/
12        └── file.go

cmd/cli/main.go:

 1package main
 2
 3import (
 4    "os"
 5    
 6    "cli-tool/internal/command"
 7    "github.com/spf13/cobra"
 8)
 9
10func main() {
11    rootCmd := command.NewRootCmd()
12    rootCmd.AddCommand(
13        command.NewGenerateCmd(),
14        command.NewValidateCmd(),
15    )
16    
17    if err := rootCmd.Execute(); err != nil {
18        os.Exit(1)
19    }
20}

internal/command/root.go:

 1package command
 2
 3import (
 4    "github.com/spf13/cobra"
 5)
 6
 7func NewRootCmd() *cobra.Command {
 8    return &cobra.Command{
 9        Use:   "mytool",
10        Short: "Production-grade CLI tool",
11    }
12}

Best Practices and Common Pitfalls

  1. Start Simple: Begin with minimal structure (cmd, internal, pkg) and expand as needed
  2. Avoid Over-Engineering: Don’t create directories until you actually need them
  3. Use /internal Wisely: Place business logic here to prevent unwanted imports
  4. Keep /pkg Public: Only export what others need to consume
  5. Testing Strategy:
    • Put TestMain in package directories
    • Use /test for integration tests requiring multiple packages

Common Mistakes:

  • Placing business logic in /cmd (should be in /internal)
  • Making /pkg a dumping ground for unrelated components
  • Ignoring Go’s package naming conventions

When Not to Use:

  • Tiny single-binary projects
  • Short-lived prototypes
  • When your team has established different conventions

Conclusion

The golang-standards/project-layout provides a valuable starting point for Go projects needing structure at scale. While not mandatory, its patterns solve real organizational challenges in production systems.

Adopt this layout when:

  • Building long-lived applications
  • Developing shared libraries
  • Working with teams that need clear code boundaries
  • Integrating complex systems (CI/CD, multiple binaries)

For smaller projects, consider simplifying the structure while maintaining key principles like separation of concerns and package visibility.

Resources:

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy