Skip to content
beginner Phase 7 · Packages, Modules, and Project Layout

Imports, goimports, and go mod tidy

Organize import blocks and keep module requirements in sync with source files.

30m
0 problems
Topic Progress 0%

The Five-Line Conflict in Every Pull Request

Every pull request in a busy Go repo conflicts in the same place: the import block. Two developers add different packages to the same file, git cannot merge both insertions, and the resolution is mechanical. The conflict reveals what the block actually is — a file's declaration of the packages it depends on, ordered by convention so a reviewer sees the dependency edges without reading the body.

package checkout

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/stripe/stripe-go/v82"
	"golang.org/x/sync/errgroup"

	"example.com/shop/inventory"
	"example.com/shop/orders"
)

Three groups, each alphabetized, each separated by a blank line: the standard library, third-party modules, and packages from your own module. The grouping is not enforced by gofmt — the formatter never reorders your imports. Teams adopt it because a reviewer glancing at the block learns instantly whether a change introduces a network dependency or stays in the standard library. Conflicts also shrink: developers editing different groups no longer collide.

Two facts hide inside each import line. First, the import path and the package name are different things. The identifier you write comes from the package clause inside the source file, which usually — but not always — matches the last path segment:

import (
	"database/sql"            // package name: sql
	"github.com/go-sql-driver/mysql" // package name: mysql
	"gopkg.in/yaml.v2"        // package name: yaml (path ends in v2)
)

When two imported packages share a name, alias one. Alias only on collision; gratuitous aliases make readers check the mapping twice.

Second, an unused import is a compile error, not a warning. Go refuses to build a block that disagrees with the body, which means a stale import after a refactor breaks the build the moment it exists instead of lingering for months. That strictness is also why tooling can safely manipulate the block: if the code compiles, every import in the block is used, and every package the code names must be importable. Keeping those two directions in sync is mechanical work, and mechanical work belongs to tools — which is where gofmt, goimports, and the module commands come in.

gofmt Rewrites Whitespace; goimports Rewrites Dependencies

A refactor just landed: a helper package was deleted and a new JSON field added. On the author's machine the build is green; on a teammate's fresh clone it fails with undefined: json.MarshalIndent. The code says json.MarshalIndent, the import block does not say encoding/json, and nobody wants to hand-edit blocks on fifty files. Two tools exist for this, and they do different jobs.

gofmt reformats source to the canonical layout and touches nothing else:

gofmt -l .      # print files whose formatting differs; empty output = clean
gofmt -w .      # rewrite them in place

It never adds or removes imports. After the refactor above, gofmt -w leaves the missing import missing.

goimports is gofmt plus import hygiene. It parses each file, works out which imports the body uses, drops the unused ones, and adds the ones it can resolve:

// before: uuid is no longer referenced, json is now referenced
import (
	"os"

	"github.com/google/uuid"
)

// after `goimports -w .`
import (
	"encoding/json"
	"os"
)

Because it needs to know which packages exist, run it inside a module — it consults the module's requirements and cache to resolve third-party names, and it resolves standard library names unconditionally.

The one flag worth memorizing is -local, which routes your own module's packages into their own trailing group:

goimports -local example.com/shop -w .

Now the block always reads stdlib, third-party, yours — on every machine, regardless of who saved last. Wire both checks into CI so formatting never becomes a review comment:

test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
out=$(goimports -local example.com/shop -l .) && test -z "$out" || { echo "$out"; exit 1; }

One honest limitation: goimports resolves by name. If two modules export the same identifier, it can guess wrong, so read the diff of any import change it makes. A wrong import fails compilation loudly, which is the acceptable failure mode — but you still want the diff in review, not just in your editor.

go mod tidy: Making go.mod Describe What the Code Actually Imports

A fresh clone fails to build: no required module provides package github.com/pkg/errors; to add it: go get github.com/pkg/errors. The author's editor auto-imported the package weeks ago with -mod=mod, quietly updated their local go.mod, and the change was never committed. The teammate's checkout has the new import and the old requirement list. Two files must move together — go.mod declares the requirements, go.sum records the hashes that prove them — and only one made it into the commit.

go mod tidy reconciles both files with the source:

go mod tidy     # add missing requirements, drop unused ones, refresh go.sum
go mod verify   # check the module cache against go.sum hashes
go get github.com/acme/logfmt@v1.4.0  # pin a chosen version on purpose

What tidy scans is the part people underestimate. It walks every package in the module — including the imports inside test files — resolves each import path to a module, adds requirements for modules nothing declared (marked // indirect when they are only transitive), removes direct requirements nothing imports anymore, and regenerates go.sum entries for the versions it now considers. A library imported only by a _test.go file still counts; tidy will keep its requirement alive.

Deliberate version choices survive tidy: it adds requirements for what is missing and removes what is unused, it does not gratuitously upgrade what is already required. Use go get pkg@vX.Y.Z when you want a specific version, and tidy will leave it alone as long as the code still imports it.

For CI failures, the diagnosis fits in a table:

Error What is true Fix
no required module provides package X code imports X, go.mod does not require its module go get X or go mod tidy, commit go.mod
missing go.sum entry for go.mod/file requirement changed without hash records go mod tidy, commit go.sum
updates to go.mod needed; run 'go mod tidy' same as above, detected by the toolchain go mod tidy

The discipline that prevents all three: run tidy after any import change and commit go.mod and go.sum in the same commit as the import. Neither file is hand-edited — hashes in go.sum are computed, and manual entries fail verification. What tidy cannot tell you is which version you should require, how a dependency's requirements combine into the final build, or why a v2 library needs a new import path — that is the subject of go.mod and Semantic Versioning. The import blocks themselves follow from Packages and Exported Identifiers.

Quiz

1. You delete the last reference to github.com/pkg/errors from a file. Which command removes the now-unused import line?

Question 1 options

2. CI on a fresh clone fails with: `no required module provides package github.com/acme/retry; to add it: go get github.com/acme/retry`. The code compiles locally. What happened?

Question 2 options

3. Your team wants standard library imports first, third-party modules second, and your own example.com/shop packages in a trailing group — enforced automatically. Which setup does it?

Question 3 options

4. A package is imported only inside a `_test.go` file of your module. What does `go mod tidy` do with the module providing it?

Question 4 options

Flashcards

Question

After a refactor, which tool removes unused imports and which only fixes formatting?

Answer

goimports removes unused imports and adds missing ones because it parses each file and compares the block against the identifiers the body uses. gofmt only canonicalizes whitespace and layout — it will happily leave a stale import in place.

Question

What is the difference between an import path and a package name?

Answer

The import path is how the module and directory are located (gopkg.in/yaml.v2). The package name is the identifier you actually write in code, and it comes from the package clause inside the file — usually the last path segment, but not always. A path ending in /v2 still provides package yaml.

Question

A teammate's build fails with 'missing go.sum entry' after your dependency change. What is the real mistake?

Answer

go.mod was updated without regenerating (or without committing) the go.sum hashes that prove the new content. Run go mod tidy to restore the entries and commit go.mod and go.sum together — the pair is one unit of change.

Question

Which files does `go mod tidy` scan when deciding which requirements your module needs?

Answer

Every package in the main module, including imports that appear only in _test.go files. That is why a library used solely by tests keeps its requirement and why removing a test import can make tidy drop a dependency.

Question

What is the difference between `go get pkg@v1.4.0` and `go mod tidy` for version selection?

Answer

go get is a deliberate instruction: require this exact version (or newer within the range you name). tidy is reconciliation: it adds whatever is missing, drops what nothing imports, and refreshes go.sum — but it does not upgrade requirements the code already satisfies.

Revision Notes

Key Takeaways

  • 1. The import block is the file's dependency declaration; grouping stdlib / third-party / local makes review instant and shrinks merge conflicts
  • 2. gofmt formats, goimports edits the dependency block — they are complementary, not interchangeable
  • 3. go mod tidy scans imports including test files, adds missing requirements, drops unused ones, and refreshes go.sum
  • 4. go.mod and go.sum are a committed pair; neither is ever hand-edited because hashes are computed
  • 5. Editors that auto-update go.mod are the usual root cause of 'works locally, fails on fresh clone'

Interview Tips

  • • "How do you keep imports consistent across a team?" — goimports with -local in the editor plus a CI gate that fails on `gofmt -l` output; mention that the check is deterministic so nobody argues in review
  • • "What does go mod tidy actually do?" — walk the scan: every package including tests, resolve imports to modules, add missing requires as indirect where appropriate, drop unused, regenerate go.sum
  • • "Walk me through a 'missing go.sum entry' failure" — requirement changed without its hash records; fix with tidy, commit both files, then explain why go.sum cannot be hand-written
  • • "gofmt or goimports?" — gofmt for pure layout, goimports when import hygiene should be automatic; note the -local flag and the caveat that name collisions can make it guess

Cheat Sheet

Imports, goimports, and go mod tidy

Command table:

Task Command
List misformatted files gofmt -l .
Fix formatting only gofmt -w .
Fix formatting + imports goimports -w .
Keep own-module imports in a trailing group goimports -local example.com/shop -w .
Reconcile go.mod/go.sum with code go mod tidy
Pin a version on purpose go get pkg@vX.Y.Z
Verify cache against go.sum go mod verify

Import grouping convention (blank line between groups, alphabetical within):

import (
	"context"
	"fmt"

	"github.com/stripe/stripe-go/v82"

	"example.com/shop/inventory"
)

CI gate:

test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
out=$(goimports -local example.com/shop -l .); test -z "$out" || { echo "$out"; exit 1; }

Diagnosis: no required module provides package → stale go.mod, run go get/go mod tidy. missing go.sum entry → hash records missing, run go mod tidy. Commit go.mod and go.sum in the same commit as the import change.

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading