Start with one file
A Go program can live entirely in one main.go, and small tools should. This complete program parses input, does its work and prints a report, in one package:
When it grows, split it into more files in the same directory and the same package (parse.go, report.go, main.go). Files in one package share every identifier, so nothing needs to be exported or imported. That is often all the structure a project of a few thousand lines needs.
Remember that a multi-file package main must be run as a package: go run ., not go run main.go. Otherwise the files are compiled alone and you get undefined errors for names defined in the other files.
There is no mandated layout
Go does not require any directory structure beyond "one package per directory". The official guidance is the page Organizing a Go module on go.dev, and it describes a few shapes, not rules.
The GitHub repository golang-standards/project-layout is widely copied and widely mistaken for a standard. It is a community collection of conventions from large projects, and Russ Cox, then tech lead of Go, opened an issue there stating that it is not a Go standard. Most of its directories (pkg/, api/, build/, deployments/) make sense only for big codebases. Copying the whole skeleton into a new project creates empty folders and deep import paths for no benefit.
The rules that are enforced by the tool are short:
- One directory is one package. All
.gofiles in it (except_test.gofiles using the_testsuffix) share a package name. - The import path is the module path plus the directory path.
- A directory named
internalrestricts who can import what is inside it. - Directories named
testdata, and those starting with.or_, are ignored by the go tool.
The common shapes
A single command
todo/
├── go.mod module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go
Flat, everything in package main. go install github.com/you/todo@latest works and the binary is named todo.
A library
slug/
├── go.mod module github.com/you/slug
├── slug.go package slug
├── slug_test.go
├── example_test.go
└── internal/
└── table/ helpers the public package uses, hidden from users
The package sits at the module root, so users import github.com/you/slug and call slug.Make(...). Anything you do not want to support as public API goes under internal/.
A service or several binaries
shop/
├── go.mod module github.com/you/shop
├── cmd/
│ ├── shop-api/
│ │ └── main.go package main: flags, config, wiring
│ └── shop-worker/
│ └── main.go
├── internal/
│ ├── order/ package order: domain types and logic
│ │ ├── order.go
│ │ └── order_test.go
│ ├── store/ package store: database access
│ └── httpapi/ package httpapi: handlers, routing
├── migrations/
└── README.md
cmd/<name>/main.go holds one directory per binary, and the directory name becomes the binary name (go build ./cmd/shop-api produces shop-api). Each main stays thin: read configuration, construct dependencies, start the server. The real code lives in packages under internal/, where both binaries can use it and no other module can.
This is the layout most Go services converge on. Reach for it when you have a second binary or a real reason to split packages, not on day one.
internal/ is enforced by the go command
A package under internal/ can be imported only by code in the tree rooted at the parent of internal. From another module, the build fails:
package example.com/b
main.go:6:2: use of internal package example.com/a/internal/secret not allowed
That makes internal/ the tool for keeping your API surface small. Code there can change freely, because you know every caller is in your own repository. For an application, putting nearly everything in internal/ is reasonable. For a library, it separates what you promise to keep stable from what you do not.
internal works at any depth: shop/internal/order is visible to all of shop/, while shop/internal/order/internal/pricing is visible only inside shop/internal/order/.
Naming packages
The package name is part of every call site, so it matters more than the directory tree.
- Short, lowercase, one word:
order,store,httpapi. No underscores and no mixedCaps. - Name it for what it provides, not what it contains.
util,common,helpers,miscandmodelssay nothing and become dumping grounds; the Go team's style guidance explicitly discourages them. Put a helper in the package that uses it, or in a package named for its purpose (slug,retry). - Avoid stutter. Callers write
order.Orderandorder.New, so do not name thingsorder.OrderServiceororder.NewOrder. The standard library does this consistently:http.Server, nothttp.HTTPServer. - The directory name and the package name should match, apart from
package mainin command directories. When they differ, readers have to open a file to learn what identifier the import introduces.
Split packages by responsibility, not by kind. A models/, controllers/, services/ split (common in other ecosystems) forces every feature to touch three packages and tends to create import cycles, which Go forbids. Packages organized around a domain concept (order, payment, user) each hold their types and logic together.
Where tests go
Tests live beside the code in the same directory, in files named *_test.go. There is no tests/ tree.
package orderinorder_test.go: an internal test, which can use unexported identifiers.package order_testin the same directory: an external test, which sees only the exported API, as a caller would. Useful for examples and for avoiding import cycles in tests.- Fixture files go in a
testdata/directory next to the tests. The go tool ignores it, and tests run with the package directory as the working directory, so relative paths liketestdata/big.jsonwork.
Other files in the root
| File or directory | Purpose |
|---|---|
go.mod, go.sum | module definition and dependency checksums, always at the root |
README.md, LICENSE | as in any project |
Makefile or Taskfile.yml | optional build shortcuts |
Dockerfile | usually at the root for services |
migrations/, web/, docs/ | non-Go assets, named for what they hold |
tools.go | older way to pin tool dependencies; Go 1.24 replaces it with tool lines in go.mod (go get -tool) |
go.work | a workspace for developing several modules together locally; usually not committed for single-module projects |
Common mistakes
- Copying a big template for a small project. Start flat and add directories when a second binary or a real boundary appears.
- Packages named
utilsorcommon. Name packages for what they do. - One package per file or per type. Go packages are meant to be larger units than Java classes. A package with ten files is normal.
- Import cycles. Two packages cannot import each other. It usually means they belong together, that a shared type should move to a lower-level package, or that one side should depend on a small interface instead of the other package.
pkg/by reflex. It adds a segment to every import path without adding meaning.- Tests in a separate directory. They cannot see unexported code there and the tooling does not expect them.
Frequently Asked Questions
Is there an official Go project layout?
Not a mandatory one. The Go team publishes guidance at go.dev/doc/modules/layout, which describes a few common shapes (a single package, a command, several commands with internal/). The popular golang-standards/project-layout repository is a community project, not a Go standard, and the Go team has said so publicly.
What is the internal directory in Go?
A package whose import path contains an internal element can only be imported by code rooted at the parent of that internal directory. example.com/app/internal/store can be imported anywhere under example.com/app/, and the go command rejects imports from any other module with use of internal package ... not allowed.
Should I use a pkg directory in Go?
It is optional and adds a path segment without adding meaning. Some large projects use pkg/ to separate public library code from the rest, but the Go standard library and most modern projects do not. Put importable packages at the module root or in named directories, and anything private in internal/.
Where do test files go in a Go project?
In the same directory as the code they test, named *_test.go. Go has no separate test tree. Fixture files go in a testdata directory next to the tests, which the go tool ignores when looking for packages.