Last Updated: May 22, 2026
An import statement brings the names exported by another package into the current file so you can call its functions, use its types, and reference its constants. The path inside the import is the canonical identifier for that package: it points to a directory, either inside the standard library, inside your own module, or inside a third-party module that's been downloaded into the local module cache. This chapter covers the full shape of import declarations, the difference between an import path and a package name, and the smaller features around imports (aliasing, blank imports, dot imports) that come up in real code.
The simplest form imports one package on one line:
The string "fmt" is the import path. The identifier you use in code (fmt.Println) is the package name, which comes from the package fmt declaration at the top of the files inside the fmt package's source directory. For most standard library packages, the import path's last segment matches the package name, so the two look identical, but they are different things and the chapter comes back to that distinction below.
When a file needs more than one import, the grouped form is the idiomatic style:
The parenthesized block is one import declaration that lists multiple paths. Each path goes on its own line. gofmt sorts the lines alphabetically and removes duplicates, so you don't have to think about the order yourself. Most Go codebases use the grouped form even when there's only one import, but the single-line form is fine when there really is just one and you find it clearer.
A common convention, enforced by tools like goimports and golangci-lint, is to separate standard library imports from third-party imports with a blank line:
(The UUID will be different every run; it's a random value by design.)
Two import groups, separated by a blank line. The first holds standard library packages (encoding/json, fmt, strings). The second holds external modules (github.com/google/uuid, github.com/shopspring/decimal). goimports also recognises a third group convention for the module's own internal packages, which the next section returns to.
The path inside an import declaration is a string, and it identifies a directory containing Go source files. The identifier you write in code to reference that package's exports comes from the package declaration in those files, not from the path itself. For the vast majority of packages the two match, but they don't have to.
Take "gopkg.in/yaml.v3". The import path ends in yaml.v3, but the package's source files begin with package yaml, so in code you write yaml.Marshal, not yaml.v3.Marshal. The same shape appears in any module whose path ends with a version suffix like /v2, /v3, or /v4. The next chapter on module versioning covers why these suffixes exist; for now the relevant point is that the last path segment is a hint, not the truth.
A simpler example uses github.com/google/uuid. The path's last segment is uuid, and the source files declare package uuid. So the import path's final segment and the package name match, which is the common case:
The path tells Go where to find the source code. The package name (uuid) is what your code types when calling its functions. If the path and package name disagree, the compiler tells you exactly what name to use, and you can also look at the package's documentation on pkg.go.dev, which always shows the actual package name at the top.
The diagram shows the three places an import path can resolve to. Standard library paths (fmt, strings, encoding/json) come from the toolchain installation, the GOROOT/src directory. Third-party paths (github.com/google/uuid) come from the local module cache, populated by go get and go mod download. Paths that start with your module's own path (declared in go.mod) resolve to directories inside your project. The import keyword treats all three the same way; only the resolver internally cares which kind it is.
When you organise a project into multiple packages, each package lives in its own subdirectory of the module. To import one from another, you use the module path plus the subdirectory. There's no "relative" import syntax in Go (no import "./cart"), and there hasn't been since modules launched in Go 1.11. The path always starts with the module path declared in go.mod.
Say go.mod declares module github.com/store-co/shop, and the project tree looks like this:
Inside cart/cart.go:
Inside main.go:
The import path "github.com/store-co/shop/cart" reads like a URL, but no network request happens at build time. Go knows that the path starts with the module path from go.mod, so it looks in the local cart/ directory rather than the module cache. Moving the project to a new GitHub repo means renaming the module path and updating every internal import, which is why most teams settle on a module path early and don't change it casually.
A third import group with a blank line above it is the convention for these internal-module imports:
goimports doesn't add this third group automatically (it only groups stdlib vs the rest), but linters in larger projects often do. The grouping makes it easy to scan and see which imports are external dependencies versus which are sibling packages.
Sometimes two packages share the same final path segment, and the compiler can't tell them apart in code. Or a package name is awkwardly long. Or the package name happens to clash with a local variable. The fix is an import alias: a name written before the path that overrides the default identifier:
(The output appears in red in a terminal that supports ANSI colour. In a non-colour terminal, the escape sequences print as literal characters.)
The form is alias "path". Inside this file, the package is now called f instead of color. Other files in the same package main are unaffected; the alias applies only to the file that declared it.
A more common reason to alias is conflicting package names. Both crypto/rand and math/rand use package rand. Trying to import both in the same file produces a compile error:
An alias on one of them fixes it:
mrand refers to math/rand, and the unaliased rand refers to crypto/rand. The convention in the standard library and most Go code is to alias the one that's less idiomatic for the current file's purpose: if your file is mostly cryptographic, keep rand for crypto/rand and alias math/rand as mrand. Other versioned paths like gopkg.in/yaml.v3 get aliased too when a project also imports an older gopkg.in/yaml.v2.
Aliases live in the file they're declared in. If you alias f "github.com/fatih/color" in one file and not another, the same package will be called color in some files and f in others. That's legal but confusing. Pick one name per project and stick with it, ideally as the canonical name unless there's a real conflict.
Sometimes you want a package's init() function to run, but you don't want to call any of its exported names. The blank identifier _ as an alias does exactly that: it imports the package for its side effects, runs the init() blocks, and makes the package name unavailable in code.
The classic case is a database driver. Go's database/sql package defines a generic interface but doesn't ship drivers. To use Postgres, you import github.com/lib/pq purely so its init() registers the "postgres" driver name with database/sql. You never call pq.Anything directly in your code:
(This connects to a real Postgres instance only if one is running at the URL. The sql.Open call itself only validates the driver name and the URL syntax; it doesn't actually dial the database until a query runs.)
If you wrote import "github.com/lib/pq" without the blank, the compiler would refuse to build the file because the import is unused. With the blank, the compiler keeps the import (so the init() runs at startup) but silences the unused-import check.
The same pattern shows up in the standard library. image/png and image/jpeg register decoders with image.Decode through their init() functions. If you want to call image.Decode and have it handle PNG and JPEG inputs, you blank-import both packages:
Without the _ "image/png" import, image.Decode wouldn't know how to recognise PNG bytes, and the call would fail with image: unknown format. The blank imports look unusual at first, but they're the documented way to opt into format support without your code referring to the packages by name.
The blank import is the only legal way to import a package without using it. Aliasing it to a regular name doesn't help: import x "fmt" followed by no use of x still triggers the same unused-import error.
A dot import brings every exported name from the imported package directly into the current file's namespace. Instead of fmt.Println("hi"), you write Println("hi"). The syntax is import. "fmt".
This compiles and runs, but almost no real Go code does this. Stripping the package prefix loses the visual signal of where a function comes from, makes greps and IDE jumps harder, and creates name collisions the moment two dot-imported packages both export the same identifier. The official Go style guide and effective Go both recommend against dot imports outside of tests.
The exception is tests. A test in package cart_test (an external test package) that wants to call cart's exported names without prefixing every line can use import. "github.com/store-co/shop/cart". Even there, most projects don't, but it's the one place where the convention isn't a hard "never".
Don't use dot imports outside of tests.
Go treats every unused import as a compile error. Removing the call but leaving the import behind doesn't compile:
The fix is to either use the import or remove it. The cure is so trivial that most editors run goimports on save and remove unused imports automatically. The compiler's strictness is deliberate: dead imports add to build time, can mask refactoring mistakes, and clutter the import block, so the language refuses to let them survive.
The error message names the path, not the package identifier. If you aliased it as f "fmt", the error still reads imported and not used: "fmt". The blank identifier is the only way to keep an import that nothing calls.
The same rule applies to local variables and to imported names in dot-import form. The whole language treats "declared but never used" as a build-stopping mistake. Go's authors picked this over the more lenient "unused" warning that most languages emit because unused code rots and gives reviewers and tools false signals.
Go bans import cycles outright. If package A imports package B, and package B (directly or transitively) imports package A, the compiler refuses to build either of them. The error is loud and specific.
Here's a minimal two-package cycle. Say go.mod declares module github.com/store-co/shop, with this tree:
cart/cart.go:
pricing/pricing.go:
cart imports pricing for the Apply function. pricing imports cart for the Item type. Running go build produces:
The compiler prints the full chain so the offending edge is easy to spot. There's no way to "force" a cycle through. The fix is structural: pull the shared type into a third package that both can import, or invert the dependency so only one direction holds.
A common fix in this example is to put Item in its own package, say model, and have both cart and pricing import model:
Now cart and pricing can sit at the same level and reference model.Item without depending on each other. The cycle is broken because the dependency arrows all flow downward to model.
Why does Go forbid cycles when other languages allow them? Two reasons. First, Go's compiler processes packages in dependency order, and a cycle has no valid order. Second, cyclic dependencies tend to indicate a design problem: two packages that depend on each other could often be one package, or they share a concept that wants its own home. The forced refactor catches a class of design mistakes early.
The top two nodes show the broken design: cart and pricing point at each other, and the compiler rejects it. The bottom three show the fix: both packages depend on a shared model package, which depends on neither, and the cycle is gone.
go getAdding a third-party package to your project happens in two steps: write the import line, and run go get to fetch the module and record it in go.mod. Either order works; in practice many developers add the import first, let their editor complain about the missing module, and then run go get.
This downloads the module, writes its current latest version to go.mod, and writes the cryptographic checksum to go.sum. The next go build finds the module in the local cache and compiles it into the binary. (The go-modules chapter covered what go.mod and go.sum look like in detail.)
You can pin a specific version:
The @version syntax accepts tags, branches, or pseudo-versions. For now, @<tag> for a release tag is the common case.
To upgrade dependencies, -u asks for the latest minor or patch release:
The first form upgrades one package. The second form upgrades every dependency the current module uses. Be careful with -u./... on a large project; it can bring in a wave of changes you didn't intend to review.
To remove a dependency, drop every import of it from your code and run:
go mod tidy walks the source files, builds the actual import graph, and rewrites go.mod and go.sum to match. Unused modules disappear, and any missing ones get added. Most Go projects run go mod tidy as part of CI so the module files never drift.
A more direct way to remove a single dependency, even before go mod tidy exists in the workflow, is:
The @none query tells Go to drop the module. It's less common in practice because go mod tidy handles the case automatically, but it's there for scripts that need explicit control.
go get without a version (e.g., go get github.com/google/uuid) picks the latest release tag at the moment you run it. Two developers running the same command a week apart may end up on different versions, which is why go.sum and the version in go.mod exist: the recorded version pins the build to a specific commit.
goimports Toolgofmt formats Go source code but doesn't touch imports. goimports is a sibling tool that does both: it runs gofmt and also adds missing imports, removes unused ones, and groups the import block by convention. Most editors run goimports on save by default once Go support is configured.
Install it with:
Run it on a file or directory:
The -w flag writes the changes back to the file. Without it, goimports prints the formatted code to stdout, which is useful for diffing.
A typical save-on-format flow with goimports: you type uuid.New() into a file that doesn't yet import uuid. The editor invokes goimports, which sees the unresolved name, finds github.com/google/uuid in your module's known dependencies (or in the standard library's known packages for stdlib names), and adds the missing line to the import block. Saving the file applies the change in place. If a previous import is no longer used, goimports strips it in the same pass.
goimports doesn't fetch modules; it only edits the import block. If the missing package isn't already in go.sum or the standard library, goimports can't help. That's where go get comes in, and the two tools combine to give a workflow where you barely touch the import block by hand.