golangtutorialAll tutorials

Golang File Structure for Modules and Applications

Learn a practical Golang file structure that starts small, separates commands from domain packages, and uses internal boundaries only when useful.

Fundamentals · Lesson 7Saved in this browser. No account required.

A useful Go project structure makes package responsibilities obvious without creating empty architecture layers. You will start with one command, extract a tested domain package, and add cmd and internal only when the application needs those boundaries.

The example supports Go 1.22 and later and was compiled and executed with Go 1.24.7 on Windows amd64.

Begin with modules, packages, and files

A module is the versioned unit named by go.mod. A package is a directory of Go files compiled together. A file is a source unit inside that package. Confusing these levels leads to directory trees that fight the toolchain.

For a small command, this is enough:

orders/
  go.mod
  main.go

Keep related code together until a separate responsibility appears. The official module organization guide shows recommended patterns for packages and commands. The complete Go tutorial explains the package and visibility rules behind them.

Add directories when boundaries become real

The following structure supports one executable and one domain package:

orders/
  go.mod
  cmd/
    orders/
      main.go
  internal/
    orders/
      total.go

cmd/orders contains the executable entry point. internal/orders contains order rules that code outside the parent module cannot import. That restriction is enforced by the Go toolchain, which makes internal useful for implementation details that are not a public library API.

The module file names the import root:

module example.com/orders

go 1.22

Do not add cmd when there is only one tiny main.go and no likely second command. Do not add internal merely because popular repositories have one. Every directory should communicate a boundary that exists today.

Build the isolated domain package

Place the price calculation in internal/orders/total.go:

package orders

import "fmt"

func Total(prices []float64) (float64, error) {
	if len(prices) == 0 {
		return 0, fmt.Errorf("order total: no prices provided")
	}

	total := 0.0
	for _, price := range prices {
		if price < 0 {
			return 0, fmt.Errorf("order total: negative price %.2f", price)
		}
		total += price
	}
	return total, nil
}

The package owns validation and calculation. It does not read flags, print output, or know how the command is delivered.

Connect the application entry point

Place orchestration in cmd/orders/main.go:

package main

import (
	"fmt"

	"example.com/orders/internal/orders"
)

func main() {
	total, err := orders.Total([]float64{12.50, 5.50})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("total: $%.2f\n", total)
}

Run go run ./cmd/orders from the module root.

Output:

total: $18.00

Both Go blocks were tested together as one module. This separation now pays for itself: another command can reuse the calculation without importing terminal output behavior.

Evolve the structure step by step

Add tests beside the package

Create total_test.go inside internal/orders. Go tests belong beside the code they verify unless you are deliberately testing only the public API from an external test package. The workflow in Go tools shows how to run focused and module-wide checks.

Add another command only for another executable

If the project later needs an HTTP server and a migration command, add cmd/server and cmd/migrate. Each should remain thin and delegate domain behavior to packages. A directory under cmd represents a buildable program, not a generic category.

Add public packages deliberately

A top-level package can be imported by other modules. That creates a compatibility commitment. Keep implementation packages under internal until you intend to support external callers and can define a stable API.

Keep configuration near its consumer

Avoid a global config package that every layer imports. Parse environment variables or flags near the application boundary, validate them, then pass typed values into packages. This keeps domain functions testable and reduces hidden dependencies.

Separate deployment files from Go packages

Docker files, CI configuration, database migrations, and documentation are repository concerns. Give them descriptive top-level directories only when needed; they do not need to mimic Go package boundaries.

Recognize when a monorepo changes the choice

One repository can contain one module or several. Prefer one module while packages share a release cycle and dependency policy. Multiple modules add independent versions and dependency graphs, but they also add release and tooling overhead. Split only when ownership or distribution requires that independence.

Use go work for local development across multiple modules, not as a substitute for correctly declared module dependencies. Commit a workspace file only when the team agrees that repository workflow depends on it.

Common mistakes

Copying a large layout before writing code

Empty controllers, services, repositories, and models directories create ceremony without package cohesion. Start with the command and extract packages around behavior.

Using package names such as utils or common

These names hide responsibility and tend to become dependency magnets. Name packages after the capability they provide, such as orders, billing, or httpclient.

Creating import cycles between layers

If package A imports B and B imports A, the toolchain rejects the cycle. Move shared abstractions toward the consumer, pass functions or interfaces at boundaries, and keep dependency direction clear.

What next

Run the module through the essential Go tools workflow, then use Go compilation and execution to build the command under cmd/orders. For a longer connected sequence, follow the practical beginner course.