Project Structure & Dependencies

intermediate go testing modules

A Go project is organized into a module (the unit of versioning and dependencies) made of packages (folders of related code). Get this structure right and the project stays clean as it grows.

In simple language: a module is the whole repo’s dependency bundle, a package is one folder of code.

Modules recap

We start a module with go mod init, naming it after where it lives (usually its import path).

go mod init github.com/pman47/myapp   # creates go.mod

This creates two files we should understand:

  • go.mod — lists our module name, Go version, and our direct dependencies with their versions.
  • go.sum — cryptographic checksums of every dependency (and their deps), so builds are verifiable and tamper-proof.

Versions use semantic import versioning: v1.2.3 style tags. The big rule — if a library hits v2 or higher, its major version becomes part of the import path (github.com/foo/bar/v2). That lets v1 and v2 coexist without clashing.

go get github.com/foo/bar@v1.4.0   # pin a specific version
go mod tidy                         # sync go.mod/go.sum to actual imports

Vendoring is optional — go mod vendor copies all dependencies into a local vendor/ folder so builds don’t need the network. Some teams vendor for reproducible, offline-friendly builds; many skip it and rely on the module cache.

The common layout

There’s no enforced project layout, but the community converged on a convention. Here’s the typical shape:

myapp/
├── go.mod module + deps
├── go.sum dep checksums
├── cmd/ main packages (entry points)
│   └── api/
│       └── main.go
├── internal/ private — importable only inside myapp
│   ├── auth/
│   └── db/
├── pkg/ public — safe for others to import
│   └── client/
└── README.md
  • cmd/ — one subfolder per executable, each with a package main and main.go. A repo with an API and a CLI would have cmd/api/ and cmd/cli/.
  • internal/ — see the special rule below.
  • pkg/ — library code we’re happy for outside projects to import. (Some people skip pkg/ and put packages at the root — that’s fine too.)

The special internal rule

This is the one interviewers love. internal/ isn’t just a naming convention — the compiler enforces it. Code inside an internal/ directory can only be imported by code rooted in the parent of that internal/ folder.

In simple language: internal/ means “private to this module — outsiders can’t import it.” If another repo tries to import github.com/pman47/myapp/internal/db, the build fails. It’s how we keep implementation details from leaking into other people’s code.

Package naming conventions

  • Package names are short, lowercase, single wordhttp, json, user. No underscores, no camelCase.
  • The package name should match its folder name.
  • Avoid stutter. If the package is user, name the constructor user.New(), not user.NewUser() — callers already type user., so repeating it reads as user.NewUser. Same reason it’s http.Server, not http.HTTPServer.

Avoiding circular imports

Go forbids circular imports — if package a imports b, then b cannot import a. The build just refuses. This sounds annoying but it forces clean, one-directional dependencies.

When we hit a cycle, the usual fixes are:

  • Pull the shared types into a third, lower-level package both can import.
  • Use an interface so the lower package doesn’t need to know the higher one (dependency inversion).

A cycle is almost always a signal that two packages are really one concern split badly, or that a dependency is pointing the wrong way.

Interview soundbite

A Go module (go mod init, tracked by go.mod/go.sum with semantic import versioning) groups packages, and the community layout is cmd/ for entry points, internal/ for private code, and pkg/ for public libraries. The killer detail is that internal/ is compiler-enforced — only the parent module can import it. Package names stay short and stutter-free (http.Server, not http.HTTPServer), and Go flat-out forbids circular imports, which we break by extracting shared types or introducing an interface.