File Organization Conventions

How to organize Go source files within a module, directory conventions, package and file scoping, and project layout patterns

The Core Rule: One Package Per Directory

A Go package is a collection of source files that live in the same directory and share the same package declaration. The language enforces this strictly: every .go file inside a given directory must belong to exactly one package. You cannot have two files in the same folder that declare different package names — attempting to build will result in a compiler error.

This is not a style recommendation; it is a hard requirement. The compiler uses the directory as the boundary for the package, and all files inside it must agree on the package name.

// file: mathutil/add.go
package mathutil
func Add(a, b int) int {
    return a + b
}
// file: mathutil/multiply.go
package mathutil
func Multiply(a, b int) int {
    return a * b
}

Both files are in the mathutil directory and both declare package mathutil. The compiler treats them as one logical unit. A user imports the package — not individual files — with import "example.com/project/mathutil".

Mixing Package Names in One Directory Breaks Compilation:

If you place a file with package mathutil and another with package utils in the same directory, you'll see an error like found packages mathutil and utils in .... This is one of the most common build errors for newcomers. The fix is always to move one of the files to its own directory or change the package declaration so all files match.

A helpful mental model: think of the directory as a named box. Everything inside the box must have the same label (the package name). You can put the code in as many files as you like — they are just pieces of paper in that box — but the label on the box is what the outside world sees when it imports it.

Package Name and Directory Name Convention

Idiomatic Go keeps the package name and the directory name identical. If you create a directory called auth, the files inside should declare package auth. This convention is not enforced by the compiler, but it makes code navigation predictable.

myproject/
  auth/
    login.go      // package auth
    token.go      // package auth
  go.mod

A reader who sees import "myproject/auth" expects the package to be named auth. When the two match, tools like gofmt and IDE navigation work seamlessly, and humans don't have to guess.

Exceptions exist, and they are well understood in the Go community:

  • The package main for executable commands often lives inside a directory named after the command (e.g., cmd/server/main.go with package main). The directory does not need to be named main.
  • Test files with a _test suffix (e.g., auth_test.go) can use an external test package like package auth_test, but that is a special case related to testing, not general code organization.

Stick to the Convention Unless You Have a Clear Reason:

Deviation from matching directory and package names surprises other developers. When you must deviate — for instance, in the cmd layout discussed later — document it or follow well‑known community patterns so the intent is obvious.

File Naming and Splitting Code Across Files

File names in Go follow simple conventions: lowercase letters, digits, and underscores; no spaces. The suffix _test is reserved for test files. There is no requirement that a file name matches anything inside it — animals.go can contain a type Car — but sensible naming helps humans find things.

A single file per package is fine for tiny libraries. As the package grows, splitting the code into multiple files keeps each file focused. A common practice is to give each exported type its own file, along with its methods, or to group related functionality (e.g., errors.go for custom error types, handlers.go for HTTP handlers).

notify/
  notify.go        // package notify, public API: func Send(...)
  email.go         // email provider implementation
  sms.go           // SMS provider implementation
  errors.go        // sentinel errors

Inside notify.go you might define the interface and the public Send function. The email.go and sms.go files contain unexported helper types and constructors. Because all files share package notify, the public function in notify.go can freely call unexported functions in email.go.

A Single 2000‑Line File Is a Maintenance Headache:

There is no compiler penalty for large files, but when you need to come back to the code months later, finding the function that handles email bounces is much easier if it lives in bounce.go rather than somewhere in everything.go. Early splitting costs almost nothing and pays off quickly.

This leads into the next subtle point: file boundaries in Go do not create separate namespaces. All files in the same directory share the same package scope. An unexported variable defined in one file is visible in every other file of that package. The file is just an organizing tool for the developer, not a visibility barrier.

Package-Level Scope: No File Boundaries

A frequent source of confusion for developers coming from languages where a file creates a module or private scope is that Go does not work that way. Every top‑level identifier — variables, constants, types, functions — declared in any file of a package is directly accessible from all other files of that same package. You never import another file from the same directory; you simply use the symbol.

// file: shapes/rectangle.go
package shapes
type Rectangle struct {
    Width, Height float64
}
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
// file: shapes/circle.go
package shapes
import "math"
type Circle struct {
    Radius float64
}
// Helper used only inside this file
func circleArea(r float64) float64 {
    return math.Pi * r * r
}
// file: shapes/shapes.go
package shapes
// Summary returns the total area of mixed shapes.
func Summary(rect Rectangle, circ Circle) float64 {
    return rect.Area() + circleArea(circ.Radius)
}

Even though circleArea is unexported (lowercase), it is still callable from shapes.go because both files belong to the same package. If you wanted to truly hide circleArea from other files in the package, you cannot — the package is the smallest privacy unit. The idiomatic way to signal “this helper is an internal detail of the package” is to keep it unexported and document that it should not be called from external packages. Developers reading the code will understand that it is not part of the public API.

No Need to Import Within the Same Package:

If you find yourself wanting to write import "./circle" inside the same directory, stop. That is never correct in Go. All files in one directory are already one package; just use the symbol directly. The import statement is only for bringing in external packages or packages from different directories.

The internal Directory Rule

Go provides a special directory name with enforced import restrictions: any path containing an element named internal can only be imported from code inside the tree rooted at the parent of that internal directory. In practice, this means you can place packages you want to keep private to your module under an internal/ folder, and the compiler will prevent anyone outside your module from using them.

myproject/
  go.mod          // module github.com/me/myproject
  main.go         // package main
  internal/
    db/
      connect.go  // package db
    config/
      load.go     // package config
  public/
    api.go        // package public

Here, main.go can import github.com/me/myproject/internal/db, but if someone forks your module and tries to import that same path from their own code, the compiler rejects it. The public/ package has no such protection — anyone can import it.

Using internal is not mandatory, but it is a powerful way to enforce modularity. It lets you refactor implementation details without worrying about breaking external consumers. If you later decide a package is mature enough to be public, you can move it out of internal/.

internal Is a Compiler‑Enforced Guarantee:

Unlike comments or naming conventions that merely communicate intent, the internal rule is checked at build time. It gives you a guarantee that no one outside your module depends on those packages.

Common Project Layout Patterns

There is no single mandated project structure for Go, but several patterns have emerged. The right choice depends on whether you are building a library, a command, or a combination of both.

Single‑package library

The simplest form: all code in the module’s root directory, one package.

mylib/
  go.mod       // module github.com/me/mylib
  mylib.go     // package mylib
  helper.go    // package mylib

Basic command

A single executable; the root directory holds main package files.

myapp/
  go.mod
  main.go      // package main
  config.go

Command with supporting packages

As the program grows, you pull logic into an internal/ directory to prevent leakage and keep the root clean.

myapp/
  go.mod
  main.go
  internal/
    parser/
      parser.go
    runner/
      runner.go

Multiple commands

When a repository contains several binaries, a cmd/ directory groups them. Shared code can live in internal/ or at the root level if reusable.

tools/
  go.mod          // module github.com/me/tools
  cmd/
    scanner/
      main.go
    reporter/
      main.go
  internal/
    shared/
      util.go

Mixed library and command

A repository that offers importable packages and an installable command places the library packages at the root (or in dedicated subdirectories) and the command inside cmd/.

myproj/
  go.mod
  myproj.go       // package myproj — importable
  auth/
    auth.go       // package auth
  cmd/
    myproj/
      main.go     // package main

The Go Modules documentation recommends keeping library code outside internal only if you intend to support it as a public API. If the server logic is purely internal, put it in internal/.

Avoid a 'utils' Dumping Ground:

A directory named utils or helpers that accumulates unrelated functions is a known antipattern. It becomes a magnet for everything that doesn’t have a clear home, making the package hard to understand. Prefer small, focused packages — even if that means creating many of them.

Setting Up a Go Module with Proper File Organization

Here is a concrete, step‑by‑step sequence that applies the conventions above. We'll create a minimal project that contains a library package and a command.

1

Step 1: Create the module root and initialize the module

Create a directory and run go mod init.

mkdir fileorg && cd fileorg
go mod init example.com/fileorg
2

Step 2: Add the library package

Create a directory for the package and add a file.

mkdir calc
package calc
// Add returns the sum of two integers.
func Add(a, b int) int {
    return a + b
}
3

Step 3: Add the command

Place the executable in cmd/myapp/.

mkdir -p cmd/myapp
package main
import (
    "fmt"
    "example.com/fileorg/calc"
)
func main() {
    fmt.Println(calc.Add(3, 4))
}
4

Step 4: Build and verify

Build the command to check that everything compiles and the package boundaries are correct.

go build ./cmd/myapp
./myapp   # prints 7

Clean Build Confirms Correct Organization:

If go build succeeds without errors, the directory layout follows Go’s conventions — the package declarations match the directory structure, and no file‑level import mistakes exist.

Common Mistakes and Misconceptions

Several missteps appear repeatedly when developers first organize Go code. Knowing them explicitly saves debugging time.

  • Mixing two package names in one directory. This is the error shown earlier. The fix is always to move one file to its own directory or align the package declarations.
  • Creating a main package in a subdirectory but not using the cmd/ pattern. It works technically, but the community expects commands in cmd/. When you place main.go in server/main.go and go install it, the binary will be named server. If you later add another command, you end up with a confusing flat structure.
  • Trying to import a file from the same directory. If you have a.go and b.go in the same folder, you never write import "./b". Both files are already part of the same package.
  • Over‑isolating with internal/ early on. While internal is useful, moving everything there prematurely can make it hard to extract a reusable library later. Start without it for code that might genuinely be shared, and pull it into internal once its public surface stabilizes.
  • Making a utils package. A few standalone helpers may seem harmless, but the package grows without a theme. When you need a function, first ask: does it belong to an existing package? If not, what specific responsibility does it serve? Create a small, focused package for that responsibility — never utils.

Ignoring Organization Conventions Causes Hard‑to‑Find Bugs:

A missing internal directory won't cause a compiler crash, but an improperly structured multi‑command project can lead to accidental dependency cycles or import path collisions that only appear at scale. Following the patterns from the start prevents these subtle issues.

Summary

File organization in Go is governed by a small set of strict rules and a larger set of community conventions. The core rule — one package per directory — is non‑negotiable; the compiler enforces it. Building on that foundation, naming directories after their package, splitting code into purpose‑driven files, and using internal for encapsulation produce a codebase that any Go developer can navigate quickly.

The strongest takeaway from this section is that files are not privacy boundaries. Everything in a directory shares the same package scope, so design your package’s internal API with that in mind. Use naming — unexported symbols — to signal implementation details, not file placement.