Import Cycles, Blank Imports & Dot Imports
Go forbids import cycles at compile time.
Blank imports run package init for registration side effects, while dot imports merge exported names into the importer's namespace and are discouraged in production code.
Summary
An import cycle happens when package A imports B and B imports A (directly or through a chain).
The compiler rejects cycles because initialization order would be undefined.
Blank imports (import _ "pkg") are for side effects only.
Dot imports (import . "pkg") expose exported identifiers without a qualifier and harm readability.
Recipe
Quick-reference recipe card - copy-paste ready.
package main
import (
"fmt"
_ "image/png" // register PNG decoder via init
)
func main() {
fmt.Println("decoders registered")
}When to reach for this:
- Registering drivers, codecs, or HTTP handlers via
init. - Breaking cycles by extracting shared interfaces to a small third package.
- Auditing imports during review when readability degrades.
Working Example
Breaking a cycle with a shared contracts package:
example.com/app/
service/service.go
store/store.go
contract/contract.go
// contract/contract.go - no imports from service or store
package contract
type User struct {
ID string
}
type Repository interface {
Save(u User) error
}// store/store.go
package store
import "example.com/app/contract"
type Memory struct{}
func (Memory) Save(u contract.User) error { return nil }// service/service.go
package service
import (
"example.com/app/contract"
"example.com/app/store"
)
func Run() {
var repo contract.Repository = store.Memory{}
_ = repo.Save(contract.User{ID: "1"})
}What this demonstrates:
serviceandstoreboth depend oncontract, not on each other.- Interfaces live in the package that defines the consumer's needs (here
contract). - Concrete types implement interfaces without importing
service.
Deep Dive
How It Works
Import cycles: The compiler builds a DAG of packages.
Any back-edge fails the build with import cycle not allowed.
Refactor by moving shared types, interfaces, or functions to a lower layer both sides import.
Blank imports: The compiler still links the package and runs all init functions in dependency order before main.
Typical uses:
database/sqldriversimage/*format registration- Plugin registration tables
Dot imports: import . "fmt" lets you write Println instead of fmt.Println.
Go style rejects this in application code because grep and readers lose package context.
Cycle Breaking Patterns
| Pattern | Move what | Trade-off |
|---|---|---|
| Extract interfaces | Consumer-facing API types | Extra package file |
| Extract DTOs | Shared structs | May widen API surface |
| Dependency injection | Construct graphs in main | More wiring in cmd |
| Event/callback hooks | Invert dependency direction | Harder to trace flow |
Go Notes
// Blank import - side effect only
import _ "github.com/lib/pq"// Dot import - avoid in app code
import . "example.com/app/config" // allows Port instead of config.Port// Detect cycles early
// go build ./... reports the import path chainGotchas
- Putting interfaces in the implementation package often invites cycles when the consumer also needs concrete types.
- Heavy init in blank-imported packages slows startup and hides failures until runtime.
- Dot imports in tests (
import . "foo") still confuse readers; prefer explicit qualifiers. - Cycles through test packages -
foo_testexternal tests can importfooand neighbors; design production packages to stay acyclic. - init order across packages follows import graph; cycles would make that undefined, hence the compile error.
Alternatives
- Explicit registration functions instead of
init-registry.Register("png", decode)called frommainfor clarity. - Wire/fx style DI - Compose dependencies in
cmdwithout mutual imports. - Build tags - Split optional integrations into separate packages imported only where needed.
FAQs
Why does Go disallow import cycles?
Package initialization runs before main.
A cycle would leave some packages half-initialized with no safe order.
When is a blank import appropriate?
When a package exists solely to register with a global registry at init time and your code never calls it directly.
Can I blank-import my own package?
Yes, if it exposes init side effects.
Prefer explicit setup in main for application wiring unless mimicking driver patterns.
Are dot imports ever OK?
Rarely in tests or generated code.
Production code should use qualified identifiers per Go Code Review Comments.
How do I find the cycle path?
Read the go build error - it prints the import chain.
Fix the lowest shared concern first.
Do interfaces always break cycles?
Only when the interface lives in a package neither side imports circularly.
Both sides can depend on the contracts package.
What about mutual package-level vars?
Still a design smell even without a cycle.
Pass dependencies explicitly or via constructors.
Can init import cycles happen with tests?
External test packages are separate compilation units.
Keep production packages acyclic; tests have more flexibility but should not drive bad layouts.
Is importing fmt and log a cycle risk?
No - stdlib packages do not import your code.
Cycles appear in your module's own graph.
Should HTTP handlers use blank imports?
Frameworks sometimes register routes via init.
Explicit route tables in main or internal/server are easier to audit.
Related
- Package Naming & internal/ Directories - Package boundaries
- Standard Project Layout for Services and Libraries - Wiring in cmd/
- Packages and Modules: Go's Unit of Distribution - Package compile model
- Packages & Modules Basics - Blank import example
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).