The first rite of passage
You just added a shiny new logging library to your Go project. You import it, write a quick function, and hit run. The terminal screams back with imported and not used: "github.com/some/logging". You stare at the code. You definitely imported it. Why is the compiler yelling?
This is the first rite of passage for every Go developer. The compiler isn't being difficult. It is enforcing a design choice that keeps Go projects lean and dependencies honest. In many other languages, unused imports are ignored. The runtime shrugs and moves on. Go takes a different stance. Every import adds to your binary size and increases the surface area for security vulnerabilities. Unused imports are dead weight. The compiler forces you to clean up that dead weight immediately.
Why Go enforces this rule
Go treats imports like physical tools in a workshop. If you bring a hammer to the job site, you must swing it. If you just carry the hammer around and never hit anything, the foreman sends you back to the shed.
This rule prevents "import creep." In large projects, code evolves. Functions get refactored. Dependencies get swapped. If unused imports were allowed, a project would accumulate a graveyard of dependencies that nobody uses but nobody dares to remove because they are afraid of breaking something. The compiler acts as a strict editor. It forces a direct link between the import statement and the usage site. If the usage disappears, the import must go.
Binary size is also a real concern. Go produces statically linked binaries. Every package you import contributes code to the final executable. While the linker is smart about dead code elimination, unused imports signal intent. They tell the compiler and the linker that you want this package. Removing unused imports helps the linker strip more aggressively. It also reduces the attack surface. Fewer imported packages mean fewer potential vulnerabilities in your dependency chain.
Unused imports are dead weight. The compiler forces you to clean up that dead weight immediately.
Minimal example
Here is the simplest case that triggers the error. You import a package and never reference any of its symbols.
package main
import (
"fmt"
"os" // This import triggers the error because os is never referenced.
)
// Main is the entry point of the application.
func main() {
fmt.Println("Hello, world")
// os.Args is available but never accessed.
// The compiler sees the import and finds zero references to the os package.
}
The compiler rejects this with imported and not used: "os". It points directly to the line causing the trouble. The program does not run. You cannot bypass this check with flags or environment variables. It is a hard error.
To fix this, you must either use the package or remove the import.
package main
import (
"fmt"
"os"
)
// Main uses the os package to access command line arguments.
func main() {
fmt.Println("Hello, world")
// Referencing os.Args satisfies the compiler.
// The os package is now explicitly used in the code.
fmt.Println("Args:", os.Args)
}
What happens at compile time
When you run go build or go run, the compiler performs a static analysis pass before generating any machine code. It builds a dependency graph of your package. For every import statement, it checks the symbol table.
The compiler scans your function bodies, type definitions, variable declarations, and struct fields. It looks for any identifier that matches the imported package name. If an imported package name appears nowhere in the code, the compiler flags it as an error. This happens at compile time. Your program never starts.
This strictness ensures that your go.mod file reflects exactly what your code needs. If a dependency is listed in go.mod but not imported, it is a transitive dependency brought in by another package. If it is imported but not used, it is a mistake in your code. The compiler distinguishes between these two cases. go mod tidy cleans up go.mod by removing transitive dependencies that are no longer needed. It does not fix unused imports in your source files. You must fix the code.
The compiler is a strict editor. It keeps your dependency graph honest.
Side effects and the blank identifier
Sometimes you import a package not for its functions, but for its side effects. Database drivers are the classic case. You do not call pq.Connect(). You call sql.Open(), and the driver registers itself in the background.
How do you import a package without using its symbols? You use the blank identifier _.
package main
import (
"database/sql"
_ "github.com/lib/pq" // The underscore imports the package for its init() side effects only.
"fmt"
)
// Main demonstrates importing a driver for side effects.
func main() {
// The pq driver registers itself with the sql package during init().
// We don't reference pq directly, but the import is valid because of the underscore.
db, err := sql.Open("postgres", "dbname=test")
if err != nil {
// Handle the error explicitly.
// The community accepts verbose error checks because they make the unhappy path visible.
fmt.Println("Error:", err)
return
}
defer db.Close()
fmt.Println("Connected")
}
When you use _ "pkg", the compiler loads the package, resolves its dependencies, and runs its init() functions. The init() function runs exactly once per package, before main() starts. This is how drivers register themselves. The sql package maintains a registry of drivers. Drivers add themselves to this registry in their init() function. Without the import, the registry is empty. sql.Open fails because it cannot find the driver.
The blank identifier _ is a powerful tool. It tells the compiler, "I know this value exists, and I am intentionally discarding it." Use it for imports when you need initialization logic. Use it for multi-return functions when you only care about one value.
Do not use _ to hide errors. If a function returns an error and you assign it to _, you are silencing a potential failure. The community frowns on _ = err. Handle the error or return it.
The underscore is a deliberate discard. Use it for side effects, not for hiding errors.
Pitfalls and conventions
Aliasing an import does not save you from this error. If you alias a package but never use the alias, the compiler still complains.
package main
import (
log "github.com/sirupsen/logrus"
)
// Main attempts to use an aliased import.
func main() {
// If you never call log.Info(), the compiler still complains.
// The alias is just a name. The package must still be referenced.
// log.Info("Hello") // Uncomment this to fix the error.
}
The compiler rejects this with imported and not used: log "github.com/sirupsen/logrus". The alias is treated as the package name for usage checks. You must reference log somewhere in the code.
Commenting out the usage does not help either. If you comment out the line that uses the package, the import becomes unused. The compiler checks the active code, not the comments.
package main
import (
"os"
)
// Main has commented out usage.
func main() {
// os.Args[0] // This line is commented out.
// The compiler sees no usage of os.
// Error: imported and not used: "os"
}
You can verify unused imports automatically using go vet. go vet runs static analysis checks that are stricter than the compiler in some cases. It helps catch logical errors and potential bugs. For imports, the compiler is the gatekeeper, but go vet is part of the standard workflow. Run go vet ./... to check your entire module.
Most editors run gofmt on save. gofmt is mandatory in the Go community. It formats code deterministically. It also removes unused imports automatically. If you delete the usage of os.Args, gofmt will strip the os import line. Trust gofmt. Argue logic, not formatting. Let the tool manage the import hygiene.
Trust gofmt. Let the tool manage the import hygiene.
Decision matrix
Use a standard import when you call functions, access variables, or define types from the package.
Use a blank identifier import (_ "pkg") when you need the package's init() function to run for side effects like driver registration.
Use an aliased import (alias "pkg") when the package name conflicts with another import or is too long to type repeatedly.
Remove the import entirely when the package is no longer needed. The compiler will force you to do this, which is a feature, not a bug.
The compiler is a strict editor. It keeps your dependency graph honest.