The Opinionated Structure of Go Projects
Emily Parker
Product Engineer · Leapcell

Introduction
In the vibrant Go ecosystem, the topic of project structure often sparks lively discussions. For many newcomers and even seasoned developers, the "Standard Go Project Layout" repository has become a de facto reference point. It prescribes a comprehensive directory structure, aiming to provide a common foundation for large, multifaceted Go projects. This initiative, while commendably trying to standardize best practices, presents a blueprint that, in practice, might be an overkill or even counterproductive for the vast majority of web applications. This article delves into why blindly adopting this "standard" layout can be detrimental to the simplicity and agility that Go champions, especially when building web services. Instead, we'll explore alternative, more fitting approaches that align better with the iterative nature and typical scale of web development.
Unpacking Go Project Structures
Before we dive deeper, let's establish a common understanding of the core concepts we'll be discussing.
The "Standard Go Project Layout"
This widely cited repository proposes a very opinionated, hierarchical structure. Its main goal is to organize a project into distinct responsibilities:
cmd/: Contains main applications for the project. Each directory undercmdis a separate executable.pkg/: Library code that's intended for general use by other projects.internal/: Private application and library code that cannot be imported by other projects.api/: API definitions (e.g., OpenAPI, Protobuf).web/: Web application specific components (e.g., static assets, templates).configs/: Configuration file templates or default configs.build/: Packaging and continuous integration.deployments/: IaaS, PaaS, and container orchestrations deployments configurations.scripts/: Scripts to perform various build, install, analysis, or other management tasks.test/: External test apps and additional test data.
The rationale behind this layout is to provide a comprehensive, enterprise-grade structure that scales with project complexity, particularly for monorepos or projects with multiple distinct executables and shared internal libraries.
Why It Might Not Be a Good Fit for Web Applications
Many web applications, especially those following modern microservices or API-first paradigms, are often more focused. They typically serve a single primary purpose, like handling HTTP requests, interacting with a database, and perhaps talking to a few external services. Let's look at specific reasons why the "Standard Go Project Layout" can become a burden.
1. Over-architecting for Simplicity
Go's philosophy, often summarized as "less is more," encourages simple, explicit designs. A complex directory structure adds cognitive overhead. For a typical web API that might have one or two main executables (cmd/api, cmd/worker), the cmd, pkg, internal separation can feel forced.
Consider a simple web service:
// main.go package main import ( "log" "net/http" ) func helloHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, world!")) } func main() { http.HandleFunc("/", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
Placing this in cmd/api/main.go, with pkg and internal directories potentially empty or scarcely populated, feels disproportionate to the actual code. The value of pkg and internal shines when you have reusable components that are genuinely shared across multiple, distinct applications within the same repository, or when you want to explicitly prevent external imports of certain packages. For a single web service, the "internal" distinction primarily adds a layer of indirection your own project structure.
2. The internal Package Debate
The internal directory is a core tenet of the "Standard Layout." Go's compiler enforces that packages in an internal directory cannot be imported by code outside the module in which internal resides. This is a powerful feature for enforcing modularity. However, for a self-contained web service, almost all application logic is internal to that service. Placing every domain model, service handler, and repository into separate internal subdirectories often fragments the codebase without a clear benefit for small-to-medium-sized projects.
Let's illustrate with a common web application structure:
Standard Layout Approach:
my-webapp/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── auth/
│ │ └── service.go
│ ├── handlers/
│ │ └── user.go
│ ├── models/
│ │ └── user.go
│ └── repository/
│ └── user.go
└── go.mod
Here, cmd/api/main.go would import internal/auth, internal/handlers, etc. While functionally correct, the internal prefix acts more as an organizational convention than a necessity for preventing external imports in a single-service project.
Alternative (Flatter) Approach:
my-webapp/
├── main.go
├── auth/
│ └── service.go
├── handlers/
│ └── user.go
├── models/
│ └── user.go
├── repository/
│ └── user.go
└── go.mod
In this flatter structure, the modules (auth, handlers, models, repository) are directly under the project root. While they are technically importable by other modules (if this were a library), for a standalone application, this is usually not an issue. The Go community often refers to this as a "flat" or "package-by-feature" layout. It leverages Go's strong package boundaries to define interfaces and hide implementation details, without the additional internal level.
3. Premature Optimization of Project Organization
Adopting a complex layout from the start can be a form of premature optimization. It might solve problems that don't yet exist for your project. As your web application evolves, refactoring and reorganizing code are natural parts of the development process. Starting with a simpler structure allows for organic growth and adaptation.
For example, if you initially have a handlers package for all your HTTP handlers:
my-webapp/
├── main.go
├── handlers/
│ ├── user.go
│ └── product.go
└── go.mod
As the project grows, and user.go and product.go become too large, you might decide to group by feature:
my-webapp/
├── main.go
├── user/
│ ├── handler.go
│ └── service.go
├── product/
│ ├── handler.go
│ └── service.go
└── go.mod
This evolution is simpler to manage when you don't have to constantly debate whether a new package goes under pkg, internal, or a new cmd entry.
4. The "Standard" Implies Authority
The title itself, "Standard Go Project Layout," carries a significant weight. Newcomers might perceive it as the only correct way to structure a Go project, leading to unnecessary complexity in simple web services. The Go team itself (creators of the language) does not endorse an official project structure, preferring to leave it to individual projects. The golang-standards organization is community-driven, not an official Go project. This distinction is crucial.
A More Pragmatic Approach for Web Applications
Many successful Go web applications opt for simpler, more pragmatic layouts. Common patterns include:
-
Flat Structure / Package-by-Feature: Grouping related code into top-level packages, often named after features or domain concepts.
my-webapp/ ├── main.go # Entry point ├── config/ # Application configuration ├── server/ # HTTP server setup, middleware ├── user/ # User-related domain, service, handler, repository │ ├── handler.go │ ├── service.go │ └── repository.go ├── product/ # Product-related domain, service, handler, repository │ ├── handler.go │ └── service.go ├── common/ # Utility functions, shared interfaces └── go.modThis approach keeps all relevant code for a feature together, improving discoverability and cohesion. The
main.gowires everything together. -
Layered/Hexagonal (Ports & Adapters) within a Flat Structure: For larger web applications that want to separate concerns more rigorously, a layered architecture can be implemented within a flatter structure.
my-webapp/ ├── main.go ├── config/ ├── internal/ # Application-specific, core logic (domain, use cases, services) │ ├── domain/ │ │ ├── user.go │ │ └── product.go │ ├── service/ # Business logic, orchestrating domain objects │ │ ├── user.go │ │ └── product.go │ └── ports/ # Interfaces for external dependencies (database, HTTP, message queues) │ ├── database.go │ └── http.go ├── adapters/ # Implementations of the ports (e.g., GORM for database, Gin/Echo for HTTP) │ ├── http/ │ │ └── handler.go │ └── persistence/ │ └── postgres.go └── go.modHere, the
internaldirectory is used for the core application logic that shouldn't be exposed or have external dependencies directly.adaptersthen connect this core to the outside world. This is a much more specific and meaningful use ofinternal.
The key is to select a structure that best serves the current scale and complexity of your project, allowing it to naturally evolve. Start simple, and introduce complexity only when it genuinely solves a problem you're encountering.
Conclusion
While the "Standard Go Project Layout" offers a comprehensive, albeit opinionated, structure suitable for complex, multi-executable projects or monorepos, it often introduces unnecessary complexity for typical web applications. Go's strengths lie in its simplicity and explicit design; a project structure should reflect these values. For most web services, a flatter, feature-oriented, or a well-reasoned layered approach within a more compact structure will enhance clarity, reduce cognitive load, and foster more agile development. Your project structure should serve you, not the other way around.

