Import Declarations
How to import packages in Go — covering import paths, qualified identifiers, grouped imports, renaming, blank imports for side effects, and dot imports.
An import declaration is how a Go source file says “I need code from another package.” Without it, the compiler doesn’t know where to find the functions, types, or variables you’re trying to use. The import system is strict by design: if you import a package and don’t use it, the program won’t compile. This keeps dependency lists clean and forces you to be intentional about what your code actually depends on.
Single import syntax
The simplest import statement lists one package path after the import keyword:
import "fmt"
You can have multiple single imports stacked one after another:
import "fmt"
import "math"
Each declaration ends at the newline. The order of imports doesn’t affect compilation, but the go fmt tool sorts them for you.
Import scope is per file:
An import declaration is visible only inside the file where it appears. If two files belong to the same package, each file must import what it needs separately.
Grouped (factored) import syntax
When a file uses several packages, writing import for each one becomes noisy. Go lets you group multiple import specifications inside parentheses:
import (
"fmt"
"math"
"strings"
)
This is called a factored import declaration. The compiler treats it exactly the same as writing separate import statements. Most Go developers use the grouped form as soon as a file has more than one import because it keeps the top of the file tidy.
The go fmt tool and goimports will automatically collapse multiple single imports into a grouped block and sort them alphabetically.
Import paths
An import path is the string inside the quotes — "fmt", "math/rand", "github.com/gin-gonic/gin". It tells the Go toolchain where to find the package on disk or over the network.
Standard library packages use short paths like "fmt", "net/http", or "encoding/json". Third‑party packages use a full path that often includes a hosting service and a repository name, like "github.com/spf13/cobra". This convention allows go get to fetch the code and go build to locate it in the module cache.
By convention, the last element of the import path is also the package name. The path "crypto/rand" points to a package whose package clause says package rand. The path "github.com/go-sql-driver/mysql" contains a package named mysql. When the convention breaks — say, a package named rand living at "crypto/rand" — Go still works because the package name comes from the source file’s package clause, not from the path.
Qualified identifiers and package names
After importing, you access exported names from that package by prefixing them with the package name:
import "fmt"
func main() {
fmt.Println("hello")
}
Here fmt acts as a qualifier. fmt.Println is a qualified identifier — fmt is the package name, Println is the exported function name.
The package name used as a qualifier is, by default, the name declared in the imported package’s package clause. The import path "math/rand" gives you a package named rand. The path "net/http" gives you a package named http. The path string itself is not used as the qualifier — only the actual package name matters.
You can check a package's real name:
Run go doc <import-path> to see the package’s declaration. For example, go doc encoding/json shows package json — confirming the name you’ll use in code.
Renaming imports
Sometimes the default package name is inconvenient. You may have two packages with the same name, or you want a shorter local alias. Go lets you supply a custom name right before the import path:
import (
crand "crypto/rand"
mrand "math/rand"
)
Now crand.Read(...) and mrand.Intn(...) are both usable without a collision.
This technique is common with logging libraries that mimic the standard log API:
import log "github.com/sirupsen/logrus"
The source file can use log.Println(...) as if it were the standard library, but the underlying implementation is Logrus. If you ever need to swap back, only the import line changes — the rest of the code stays the same.
Renaming can hide intent:
A custom name that’s shorter but less descriptive can confuse readers. Prefer the conventional package name unless you have a genuine naming conflict or a well‑established alias (like log for a logrus import).
Blank imports for side effects
Go will refuse to compile a file if it imports a package and never references any exported name from it:
import "database/sql"
import _ "github.com/lib/pq" // blank import
func main() {
// ... use sql package but never directly reference pq ...
}
The underscore _ is the blank identifier. It tells the compiler: “I know I’m not using any names from this package. That’s intentional.” The compiler will no longer complain.
So why would anyone import a package and not use its names? Some packages do important work just by being initialized. When a package is imported, Go runs its init() functions automatically. A blank import is the mechanism for triggering that initialization without pulling any identifiers into the file’s scope.
The most familiar use case is database drivers. The database/sql package exposes a generic interface, but actual driver implementations register themselves in their init() functions. By blank‑importing github.com/lib/pq, the PostgreSQL driver registers itself with database/sql. Your code never calls pq directly — it only uses sql.Open("postgres", ...). The blank import makes the driver available.
package main
import (
"database/sql"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
// ...
}
Missing a driver blank import causes a runtime panic:
If you call sql.Open with a driver name that hasn’t been registered (because you forgot the blank import), the function won’t complain at compile time. It returns a valid *sql.DB, but the first actual query will panic with “sql: unknown driver”. Always ensure the driver’s blank import is present.
Blank imports are also used to force side effects like registering HTTP handlers, profiling endpoints (e.g., import _ "net/http/pprof"), or loading configuration. The rule is simple: import with _ when you need the package’s init() to run and nothing else.
Dot imports (and why to avoid them)
A dot import drops every exported name from the imported package directly into the current file’s scope — no qualifier needed:
import . "fmt"
func main() {
Println("hello") // no fmt. prefix
}
After this import, you can write Println instead of fmt.Println. The dot form is legal but generally discouraged outside of very narrow contexts.
The main problem is readability. When a reader sees Println in the middle of a file, they can’t tell where it came from without scanning back to the imports block. In a large file with multiple dot imports, the code becomes a puzzle of invisible dependencies.
The dot import also creates a real conflict risk. If two dot‑imported packages export a function with the same name, or if one export collides with a local function, the compiler rejects the file:
import (
. "math"
. "math/rand"
)
func main() {
// ... can't use both packages — name collisions
}
This produces an error like “<name> redeclared in this block”. The compiler catches it, but it means the dot import pattern is fragile and doesn’t scale.
Dot imports are generally avoided:
The Go testing ecosystem occasionally uses dot imports — for example, importing . "github.com/onsi/ginkgo" in test files brings the Ginkgo DSL into scope without prefixes. Even then, many teams consider it a style choice worth debating. In production code, prefer explicit qualifiers.
The one legitimate use outside of testing is when writing an external test package for package a. If your test file is in package a_test and you want to access unexported identifiers from a, you can’t. So you use a dot import of a to make its exported identifiers available without a qualifier — but you still can’t reach unexported names. The benefit is mostly cosmetic; most developers just use the package qualifier.
Import scope is file‑level, not package‑level
A frequent beginner surprise: importing a package in one file of a package does not make it visible in other files of the same package. If main.go imports fmt, helper.go in the same main package must also import fmt if it needs it.
// main.go
package main
import "fmt"
func main() { fmt.Println("hello") }
// helper.go (same package)
package main
func greet() { fmt.Println("hi") } // compile error: undefined: fmt
The fix is straightforward: add the import to helper.go. This per‑file scoping encourages self‑contained source files and keeps dependencies explicit.
Import resolution is not package‑wide:
Unlike some languages where importing once affects the whole compilation unit, Go ties imports to the file block. It’s a small but intentional design choice that aids readability and avoids hidden dependencies across files.
How beginners should think about imports
Think of an import as a key. Each package you want to use has a locked door. The import path is the address; the package name is the key you hold. You must put the right key in the import statement to open that door. Once the door is open, you can call the exported functions inside — but you must use the key’s name to call them. If you lose the key (unused import), the compiler shouts at you because it doesn’t like dangling doors. If you grab keys that open multiple rooms but never walk through (blank import), you tell the compiler it’s fine — you only cared about the door opening sound (the init function).
This mental model helps clarify why unused imports are errors (unused key) and why blank imports exist (you want the side effect, not the access).
Common mistakes with import declarations
- Unused imports — the compiler stops with
imported and not used. Fix by removing the import or using a blank import if you need the side effect. - Forgetting to import a package used in another file of the same package — each file must have its own imports.
- Confusing the import path with the package name — remember that the qualifier you write is the package name (from the
packageclause), not the path. When in doubt, check withgo doc. - Dot importing two packages that export the same name — the compiler rejects it, but it’s a sign you shouldn’t be using dot imports.
- Expecting a blank import to expose names —
_ "pkg"gives no access topkg’s exported identifiers. Use a regular import if you need to call them.
Summary
Import declarations connect your code to the rest of the Go ecosystem — standard library, third‑party modules, and your own packages. The syntax is small and deliberate: single or grouped forms, custom names, blank imports, and dot imports each solve a specific problem.
The two most important takeaways are: an import is always per‑file, never package‑wide; and unused imports are errors because Go treats clarity of dependency as a hard constraint. Blank imports give you the one exception — triggering side effects without naming anything — and dot imports exist but come with a strong recommendation against casual use.