Go project structure, a practical layout

How to structure a Go project the idiomatic way: modules, the main package, internal and cmd, the pkg debate, and why staying flat early pays off.

On this page

Go has no framework-imposed folder structure, which is freeing and confusing at once. This guide shows a practical, idiomatic layout that scales from a tiny tool to a real service, and explains the trade-offs behind each choice.

Start with a module

Every modern Go project is a module. Initialise one with a module path, usually your repository URL:

go mod init github.com/yourname/greet

This creates a go.mod file that records the module path and Go version, and lists your dependencies. When you add a dependency, Go also writes a go.sum file with cryptographic checksums so builds are reproducible. Commit both files; never edit them by hand.

Keep it flat while you can

The most common mistake is over-organising too early. A small program is fine as a handful of files in one package at the repository root:

greet/
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ main.go
└── greeting.go

Only add directories when a real need appears β€” a second binary, or code you want to hide from importers. Premature nesting just adds import paths to type.

The main package and entry point

The executable lives in package main with a func main entry point. Keep main thin: parse configuration, wire dependencies together, and hand off to your real logic in another package.

// Package main is the entry point of the greet command.
package main

import (
	"fmt"

	"github.com/yourname/greet/internal/greeting"
)

func main() {
	fmt.Println(greeting.Message("Go"))
}

internal/ for private code

Go enforces one layout rule with the compiler: code under an internal/ directory can only be imported by packages rooted at internal/’s parent. It is the idiomatic way to keep implementation details private to your module.

greet/
β”œβ”€β”€ go.mod
β”œβ”€β”€ main.go
β”œβ”€β”€ internal/
β”‚   └── greeting/
β”‚       └── greeting.go
└── cmd/
    └── greet/
        └── main.go

cmd/ and pkg/: the debate

Two conventional directories are widely used but often misapplied.

  • cmd/ holds one subdirectory per binary, each with its own main.go. It earns its place as soon as you have more than one executable. With a single binary, a top-level main.go is simpler.
  • pkg/ is meant for code you intend other projects to import. It is contested: many maintainers argue it adds a directory level with no compiler meaning, unlike internal/. Prefer internal/ for private code, and only reach for pkg/ when you deliberately publish a reusable library.

Naming

  • Package names are short, lowercase, and single-word: greeting, not greetingUtils. The directory name should match the package name.
  • Avoid stutter: a function in package greeting should be greeting.Message, not greeting.GreetingMessage.
  • There is no utils or helpers grab-bag package in idiomatic Go. Name packages after what they provide.

For the official reference on modules and package design, see the documentation at go.dev, which is the source of truth as the toolchain evolves.

Summary

  • Every project is a module: commit go.mod and go.sum, never edit by hand.
  • Stay flat early; add internal/ and cmd/ only when a real need appears.
  • Prefer internal/ over pkg/ for privacy, and name packages for what they do.

Next, tighten your style with the Go conventions guide, or follow the Go developer roadmap for the bigger picture.

Search

Search runs entirely in your browser.