Importing Packages
Every non-trivial Go program is built from packages, and the import statement is how one package pulls in the code of another. Without imports, every program would be limited to the handful of built-in identifiers Go provides for free (like true, len, or error) — everything else, from printing text to parsing JSON, lives in a package you must explicitly import. Understanding exactly how import paths resolve, how the compiler links packages together, and how package-level initialization runs is essential to writing correct, well-organized Go programs.
Overview: How Package Imports Work
A Go program is organized into packages. Every source file starts with a package clause declaring which package it belongs to, and every file in the same directory must declare the same package name. The import statement, which follows the package clause, tells the compiler which other packages this file depends on and binds each one to an identifier you use to refer to its exported names.
An import path is a string, such as "fmt" or "encoding/json", that the Go toolchain resolves to an actual package. There are three broad categories of import path:
| Import path example | What it refers to |
|---|---|
"fmt" |
A package in the Go standard library, shipped with the compiler itself. |
"github.com/user/repo/pkg" |
A package inside a third-party module, downloaded and recorded in go.mod/go.sum. |
"example.com/myapp/internal/store" |
A package inside your own module, addressed by its full module path plus its directory. |
Modern Go projects are organized around a module, declared by a go.mod file at the project root (created with go mod init example.com/myapp). The first line of go.mod declares the module path, and every package inside that project is imported using that module path plus its subdirectory — there is no such thing as a relative import path like "./utils" in Go. When you import a path that isn’t part of the standard library or your own module, the go command downloads it into the local module cache and records its exact version and checksum in go.mod and go.sum, so builds are reproducible. This module-based system replaced the older GOPATH workflow, where all source code had to live under a single global workspace; you will still see GOPATH mentioned in older tutorials, but essentially all modern Go code uses modules.
Once an import path resolves to a package, the compiler links in that package’s compiled object code and exposes its exported identifiers — names that start with an uppercase letter, like fmt.Println or strings.ToUpper. Lowercase identifiers, like a hypothetical strings.trimSpace, are unexported and simply invisible outside their own package; there is no separate public/private keyword in Go, visibility is determined entirely by the capitalization of the identifier’s first letter.
Syntax
An import declaration can take several forms. Here is the general shape of each:
// Single import
import "package/path"
// Grouped imports (the idiomatic form for more than one)
import (
"package/one"
"package/two"
)
// Aliased import: refer to the package using a different identifier
import alias "package/path"
// Blank import: run the package's init() side effects only, no identifier bound
import _ "package/path"
// Dot import: inject the package's exported names directly into this file's scope
import . "package/path"
- Import path — the string in quotes; it is what the compiler resolves, never the local package name.
- Binding identifier — by default this is the imported package’s own package name (usually the last element of the path, though not always — a package’s declared name can differ from its directory name). You reference members as
identifier.Name. - alias — an explicit identifier you choose, used when the default name collides with another import or is simply inconvenient.
_(blank) — imports the package purely for its side effects (itsinit()functions run), without giving you access to any of its names..(dot) — merges the package’s exported names into the current file’s namespace so you can call them unqualified. Strongly discouraged outside of a few narrow cases like table-driven tests, because it makes it unclear where an identifier came from.
gofmt automatically sorts grouped imports alphabetically by path and will reformat them for you — you rarely need to sort them by hand.
Examples
Example 1: A single import
package main
import "fmt"
func main() {
fmt.Println("Hello, Go modules!")
}
Output:
Hello, Go modules!
This is the simplest possible import: the identifier fmt is bound to the standard library’s formatting package, and fmt.Println is one of its exported functions. The compiler links fmt‘s compiled code into the final binary.
Example 2: Grouped imports
package main
import (
"fmt"
"strings"
)
func main() {
name := "gopher"
upper := strings.ToUpper(name)
fmt.Println("Hello,", upper)
}
Output:
Hello, GOPHER
Here two standard library packages are imported in one grouped block, which is the idiomatic style once you need more than one import. Both fmt and strings are used, so the compiler is satisfied; if either were left unused, this program would fail to compile (see Common Mistakes below).
Example 3: Aliased imports to resolve a name collision
package main
import (
crand "crypto/rand"
"fmt"
mrand "math/rand"
)
func main() {
buf := make([]byte, 4)
if _, err := crand.Read(buf); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Read 4 secure random bytes:", len(buf) == 4)
n := mrand.Intn(1000)
fmt.Println("Pseudo-random number is in range:", n >= 0 && n < 1000)
}
Output:
Read 4 secure random bytes: true
Pseudo-random number is in range: true
Both crypto/rand and math/rand declare their package name as rand, so importing both under their default names would collide — the compiler would not know which rand you mean. Aliasing one (or both) to crand and mrand resolves the ambiguity. This is the single most common real-world reason to alias an import; it also documents, right at the import line, which flavor of randomness — cryptographically secure or merely pseudo-random — each call site is using.
Two other forms are worth knowing even though they don’t fit into a runnable example on their own. A blank import like import _ "image/png" is used purely to trigger a package’s init() function, commonly to register a codec or database driver with a central registry, without using any of its exported names directly. A dot import like import . "fmt" would let you call Println(...) without the fmt. prefix; it is legal but discouraged in production code because it hides where a name came from.
How It Works Step by Step
When you run go build or go run, the toolchain does roughly the following:
- Resolve import paths. For each import path, the compiler checks whether it’s a standard library package, a package inside the current module (matched against the module path in
go.mod), or an external module dependency (resolved via the module cache using the version pinned ingo.mod/go.sum). - Build a dependency graph. The compiler determines the full set of packages transitively imported by your program and the order in which they must be compiled — a package must be fully compiled before anything that imports it.
- Initialize package-level variables. Within each imported package, package-level variables are initialized in dependency order (a variable whose initializer refers to another package-level variable is initialized after that variable).
- Run
init()functions. Each imported package’sinit()function or functions (a package may have several, even across multiple files) run after its variables are initialized, before control moves to the importing package. This happens for every imported package, including ones brought in with a blank_import. - Run
main(). Only after every transitively imported package has finished its own variable initialization andinit()functions does the entry point package’s owninit()run, followed finally byfunc main().
This ordering guarantee is what makes blank imports useful: a package like a SQL driver can register itself in its init() function, and you’re guaranteed that registration has already happened by the time your main() starts, even though you never call anything on that package directly.
Common Mistakes
Mistake 1: Importing a package you don’t use
Go treats an unused import as a compile-time error, not a warning — this keeps dependency lists honest and prevents accidental bloat.
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Hello")
}
// Compile error: "os" imported and not used
The fix is simple: either use the package, or remove the import entirely.
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
Output:
Hello
Mistake 2: Shadowing a package name with a local variable
Because an import binds an ordinary identifier, that identifier can be shadowed by a local variable of the same name declared with := — after which the package is no longer reachable in that scope.
package main
import (
"fmt"
"time"
)
func main() {
time := time.Now()
fmt.Println(time)
time.Sleep(time.Second)
}
// Compile error: time.Sleep undefined (type time.Time has no field or method Sleep)
Once time is reassigned to a local time.Time value, the package identifier time is no longer visible in that scope, so time.Sleep and time.Second no longer refer to the package. The fix is to give the variable a different name.
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println("Got current time:", !now.IsZero())
time.Sleep(time.Millisecond)
fmt.Println("Slept briefly")
}
Output:
Got current time: true
Slept briefly
Mistake 3: Creating an import cycle
Go forbids two packages from importing each other, directly or indirectly — this is a compile error, not something you can work around with careful ordering.
// file: a/a.go
package a
import "example.com/myapp/b"
func UseB() { b.Hello() }
// file: b/b.go
package b
import "example.com/myapp/a"
func UseA() { a.UseB() }
// Compile error: import cycle not allowed
If package a imports b and b imports a, the compiler cannot determine a valid build order and refuses to compile either. The real fix is structural: extract the shared functionality both packages need into a third package that both a and b import, or merge the two packages if they are genuinely one concept split across a bad boundary.
Best Practices
- Let
goimportsorgofmtmanage import formatting and ordering automatically rather than hand-editing the group — most editors run it on save. - Prefer the package’s default name; reach for an alias only when there’s a genuine collision (like two packages both named
rand) or when the default name is misleadingly generic. - Avoid dot imports (
import . "pkg") in application code; they save a few keystrokes but make it much harder for a reader to know where an identifier came from. - Use blank imports (
import _ "pkg") sparingly and only when you specifically need a package’sinit()side effects, such as registering a driver or codec. - Keep import paths for your own code aligned with your module path as declared in
go.mod— rungo mod tidyregularly to prune unused dependencies and add missing ones. - Design package boundaries to form a directed acyclic graph of dependencies; if you find yourself wanting an import cycle, that’s a signal your packages are split in the wrong place.
- Remember that visibility is controlled purely by capitalization — keep implementation details unexported (lowercase) so your package’s real API surface stays small and intentional.
Practice Exercises
- Write a program that imports
strconvandfmt, converts the string"42"to an integer withstrconv.Atoi, handles the possible error, and prints the integer plus 8. Expected output:50. - Create two small packages in the same module,
shapesandreport, wherereportimportsshapesto format a shape’s name and area as a string. Import both into amainpackage and print the result. - Deliberately import a package you don’t use, observe the exact compiler error, then fix it. Next, deliberately name a local variable the same as one of your imported packages and observe how a subsequent call to that package fails to compile.
Summary
- The
importstatement binds an identifier to a package resolved from its import path — standard library, module dependency, or a package inside your own module. - Modern Go projects use
go.modto declare a module path; there are no relative imports, and the oldGOPATHworkflow is legacy. - Grouped imports are idiomatic for more than one package;
gofmtsorts them alphabetically by path automatically. - Aliased imports (
alias "path") resolve name collisions, such ascrypto/randversusmath/rand. - Blank imports (
_ "path") run a package’sinit()side effects without exposing any identifier; dot imports (. "path") merge names into your file’s scope and should be avoided in application code. - An unused import is a compile-time error, not a warning — every import must be used.
- Package-level variables and
init()functions of every imported package run, in dependency order, beforemain()starts. - Import cycles are forbidden by the compiler; fix them by restructuring package boundaries, not by reordering code.
