Import Cycles, Blank Imports & Dot Imports
Go forbids import cycles at compile time.
Search across all documentation pages
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.
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.
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:
init.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:
service and store both depend on contract, not on each other.contract).service.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/sql driversimage/* format registrationDot 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.
| 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 |
// 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 chainimport . "foo") still confuse readers; prefer explicit qualifiers.foo_test external tests can import foo and neighbors; design production packages to stay acyclic.init - registry.Register("png", decode) called from main for clarity.cmd without mutual imports.Package initialization runs before main.
A cycle would leave some packages half-initialized with no safe order.
When a package exists solely to register with a global registry at init time and your code never calls it directly.
Yes, if it exposes init side effects.
Prefer explicit setup in main for application wiring unless mimicking driver patterns.
Rarely in tests or generated code.
Production code should use qualified identifiers per Go Code Review Comments.
Read the go build error - it prints the import chain.
Fix the lowest shared concern first.
Only when the interface lives in a package neither side imports circularly.
Both sides can depend on the contracts package.
Still a design smell even without a cycle.
Pass dependencies explicitly or via constructors.
External test packages are separate compilation units.
Keep production packages acyclic; tests have more flexibility but should not drive bad layouts.
No - stdlib packages do not import your code.
Cycles appear in your module's own graph.
Frameworks sometimes register routes via init.
Explicit route tables in main or internal/server are easier to audit.
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026