The Standard Go Project Layout Debate

What Actually Works

Use a flat structure with cmd/ for apps, internal/ for private code, and pkg/ for public libraries to follow Go community standards.

The layout that survives

You start a new Go project. You create a folder, write main.go, and run it. It works. Then you add a second command, a shared library, and a test helper. Your root directory fills up with files that serve different purposes. You search for "Go project structure" and find ten different trees. One blog insists on pkg/. Another declares pkg/ dead. A third demands internal/. You stare at the screen, unsure if you need a degree in directory organization before writing your first handler.

Go itself has no opinion on your folder structure. The compiler does not care if your code lives in src/, lib/, or random_folder_123/. What matters is the module path in go.mod and the import paths inside your files. The layout debate exists because humans need patterns. A consistent layout makes it easier to find code, prevents accidental exposure of private logic, and lets tools work predictably. The community converged on a few directories that solve these problems without adding ceremony.

The standard layout is flat. It uses cmd/ for executables, internal/ for private packages, and pkg/ only when you publish a library. Everything else stays at the root or in shallow subdirectories. This structure scales from a single script to a large service without becoming a maze.

How the compiler enforces boundaries

The internal directory is the heavy lifter. When you place a package inside internal, the Go compiler enforces a hard rule: only code within the same module can import it. If you try to import myproject/internal/core from a different module, the build fails with use of internal package not allowed. This protects your implementation details. You do not need to worry about external projects depending on your private helpers.

You can nest internal as deep as you want to control scope. A package at internal/auth is private to the whole module. Any code in cmd/, pkg/, or internal/ can import it. A package at cmd/myapp/internal/config is private only to the cmd/myapp tree. Other commands cannot import it. This granularity lets you share code between commands while hiding command-specific details. The compiler checks the path at build time. If the path contains internal, the import is restricted to the subtree above it.

Public names start with a capital letter. Private names start lowercase. There are no public or private keywords. The compiler uses the first character to decide visibility. Combine this with internal/ and you get a powerful two-layer defense. internal/ blocks other modules. Lowercase names block other packages within the module.

Here's the skeleton that covers 90% of projects: a command, some internal logic, and a public package.

// go.mod
// module myproject

// cmd/myapp/main.go
package main

import (
	"fmt"
	"myproject/internal/core"
)

// main is the entry point. Go programs always start here.
func main() {
	// Import internal logic. The compiler blocks external modules from importing this path.
	result := core.Process()
	fmt.Println(result)
}

// internal/core/core.go
package core

// Process runs the core business logic. This function is exported because it starts with a capital letter.
func Process() string {
	return "Done"
}

The cmd/ directory holds executable programs. Each subdirectory is a separate binary. You do not put library code here. You do not put shared logic here. cmd/ is for main packages that wire everything together. If you have a CLI tool and a background worker, you get cmd/cli/main.go and cmd/worker/main.go. They share code via internal/. Never put main.go in the root of your module. The root should contain go.mod and maybe a few top-level packages. If you put main.go in the root, you create confusion about the module's purpose and risk import cycles.

One binary per folder. Keep cmd/ thin.

A realistic service structure

Real projects need more than a single file. They need HTTP handlers, database access, and configuration. The layout separates concerns without creating a deep hierarchy. Business logic lives in internal/. Transport code lives in cmd/ or a dedicated web/ package. Dependencies are injected through interfaces.

Here's a realistic layout for a web service. It separates the HTTP layer from the business logic and keeps database details hidden.

// cmd/server/main.go
package main

import (
	"log"
	"net/http"
	"myproject/internal/handler"
	"myproject/internal/store"
)

// main sets up the HTTP server and starts listening.
func main() {
	// Create the store. This lives in internal so external code can't access the DB directly.
	db := store.New()

	// Wire the handler. The handler depends on the store interface.
	h := handler.New(db)

	// Register routes.
	http.HandleFunc("/items", h.ListItems)

	// Start server. log.Fatal exits the program if the server fails to start.
	log.Fatal(http.ListenAndServe(":8080", nil))
}

The handler accepts an interface. This decouples the HTTP layer from the database implementation. The store returns a struct. This gives the caller a concrete value. The mantra "accept interfaces, return structs" guides most Go designs. Functions and methods take interfaces so callers can pass implementations. Functions return structs so callers get concrete values. This keeps dependencies flexible and tests simple.

// internal/handler/handler.go
package handler

import (
	"net/http"
	"myproject/internal/store"
)

// Handler holds dependencies for HTTP requests.
type Handler struct {
	// Store is the data layer. We accept an interface to decouple from the implementation.
	Store store.Store
}

// New creates a Handler with the given store.
func New(s store.Store) *Handler {
	return &Handler{Store: s}
}

// ListItems handles GET /items.
func (h *Handler) ListItems(w http.ResponseWriter, r *http.Request) {
	// Accept interfaces, return structs. The handler takes an interface, the store returns a struct.
	items := h.Store.GetAll()
	// Write response using items.
}

Tests live next to the code they test. Go does not require a separate test/ folder. Test files end in _test.go and sit in the same directory as the package. This keeps tests close to the implementation and allows them to access unexported functions for thorough coverage. You can run go test ./... from the root to execute all tests. The tooling works best when tests are co-located.

The pkg/ directory is optional. Use it when you have a library that other projects import. If your project is just an application, you probably do not need pkg/. Put shared code in internal/ or at the root. The pkg/ folder signals to other developers that the code inside is stable and safe to depend on. If you are building an app, pkg/ often adds noise. It suggests a public API that does not exist. Applications do not have public APIs in the same way libraries do. Your "public" code is just internal to the app. Use internal/ for everything except the few packages you genuinely want to share.

Don't create pkg/ until you have a reason. Applications don't need it.

Pitfalls and compiler errors

A common mistake is deep nesting. Go imports are long. myproject/pkg/core/domain/model/entity is painful to type and read. Keep import paths shallow. If you find yourself nesting more than two levels, flatten the structure. Merge folders. Drop redundant names like model or entity. The import path should describe the package's responsibility, not its file type.

Another trap is putting too much in pkg/. Teams sometimes use pkg/ to organize all shared code. This defeats the purpose of internal/. If the code is not part of your public API, it belongs in internal/. Exposing internal logic in pkg/ invites other modules to depend on it. When you refactor, you break those dependencies. The compiler cannot stop them because pkg/ has no visibility rules.

If you accidentally import an internal package from outside the module, the compiler rejects the build with use of internal package not allowed. This error saves you from leaking implementation details. If you try to import a package that does not exist, you get could not import myproject/missing (no required module provides package). The module system catches missing dependencies early.

Import paths are the backbone of Go organization. Your directory structure is essentially a map of your import tree. When you write import "myproject/internal/core", you are navigating that tree. Long import paths hurt readability. Flatten the tree. Merge adapter and database. Drop version numbers from the path and use semantic versioning in the module instead. Keep imports under three or four segments if possible.

Structure follows responsibility. Don't organize by file type.

When to use each directory

Use cmd/ when you are building an executable binary. Each subdirectory under cmd/ should contain a main package that starts one process. Keep the code in cmd/ minimal. Wire dependencies and start the server. Move logic to internal/.

Use internal/ when you have code that must not be imported by other modules. The compiler enforces this boundary, so put sensitive logic, database drivers, and private helpers here. Nest internal/ to restrict scope further.

Use pkg/ when you are publishing a library and want to mark specific packages as stable public API. If your project is an application, skip pkg/ and use internal/ or root packages instead. Only create pkg/ when you have a clear public contract for external consumers.

Use root packages when you have small, cohesive logic that does not fit elsewhere. A module with a single responsibility can live at the top level without extra folders. Keep the root clean. Limit it to go.mod, cmd/, internal/, and maybe one or two top-level packages.

Use web/ or api/ when you need to group HTTP-specific code, but keep the business logic in internal/. Domain concepts belong in the core, not in the transport layer. The transport layer should be thin.

The compiler enforces privacy. Trust internal/.

Where to go next