Fix: Go declared and not used / imported and not used (compile error)
Quick Answer
How to fix 'declared and not used' and 'imported and not used' compile errors in Go. Covers blank identifiers, goimports, gopls, build tags, conditional compilation, and common edge cases.
The Error
You try to build or run your Go code and get one of these:
Unused variable:
./main.go:7:2: x declared and not usedUnused import:
./main.go:4:2: "fmt" imported and not usedMultiple unused variables:
./main.go:8:2: err declared and not used
./main.go:9:2: result declared and not usedUnused import with alias:
./main.go:5:2: "os" imported as os and not usedBoth errors are compile errors, not warnings. Your program will not build until every declared variable is used and every imported package is referenced. There is no flag to disable this check, no linter config to turn it off, and no compiler option to downgrade it to a warning. This is intentional.
Why This Happens
Go enforces strict unused-code rules at the compiler level. The language specification explicitly states that it is a compile-time error to declare a variable and not use it, or to import a package and not reference any of its exported identifiers.
The rationale behind this design decision is straightforward. Unused variables often indicate bugs: a developer declared a variable intending to use it, then forgot, or used the wrong variable name elsewhere. Unused imports slow down compilation and bloat the binary for no benefit. Other languages like JavaScript or Python treat these as warnings (or ignore them entirely), but Go’s philosophy is to catch these problems early and force developers to keep their code clean.
This applies to local variables only. Package-level variables and function parameters are exempt from the “declared and not used” rule. You can have an unused function parameter without triggering a compile error. Similarly, struct fields, global variables, and constants do not trigger this error.
The “imported and not used” error applies to all imports, regardless of scope. If you import "fmt" and never call fmt.Println, fmt.Sprintf, or any other function from that package, the compiler will reject your code.
This strictness can be frustrating during development. You’re debugging something, you comment out a function call, and suddenly the code won’t compile because the import is now unused. Or you’re prototyping and haven’t gotten around to using a variable yet. The fixes below cover every scenario.
Fix 1: Use the Variable
The most straightforward fix: actually use the variable you declared.
func main() {
x := 42
// x declared and not used -- nothing references x
fmt.Println(x) // Now x is used
}If you declared a variable because you planned to use it later, finish that code. If you declared it by accident, delete it. If you’re in the middle of writing something and temporarily don’t need it, use one of the other fixes below to keep the code compiling while you work.
Note that simply assigning to a variable does not count as “using” it. You must read the variable:
func main() {
x := 42
x = 100 // This is still "declared and not used"
// The compiler wants you to READ x, not just write to it
}Fix 2: Use the Blank Identifier _ for Variables
The blank identifier _ tells Go you intentionally don’t need a value. This is the standard way to discard unwanted values:
func main() {
// Before: err declared and not used
result, err := someFunction()
fmt.Println(result)
// Fix: discard err with _
result, _ := someFunction()
fmt.Println(result)
}This works in several common patterns:
Discarding one return value from a multi-return function:
value, _ := strconv.Atoi("42")Discarding the index in a range loop:
for _, item := range items {
fmt.Println(item)
}Discarding the value in a range loop (when you only need the index):
for i := range items {
fmt.Println(i)
}Discarding an error you’re sure about (use with caution):
_ = json.Unmarshal(data, &config)Be careful with discarding errors. In production code, ignoring errors is a common source of bugs. If something fails silently, you’ll have a hard time debugging it. Use _ for errors only when the error truly cannot affect your program — for example, writing to a bytes.Buffer (which never fails) or in tests where you don’t care about cleanup errors. For everything else, handle the error or at least log it. This principle applies across all languages; similar vigilance is needed when dealing with TypeScript type mismatches or Python indentation issues that mask underlying problems.
Fix 3: Remove Unused Imports Manually
If you imported a package and no longer use it, remove the import:
// Before
import (
"fmt"
"os" // imported and not used
"strings" // imported and not used
)
func main() {
fmt.Println("hello")
}// After
import "fmt"
func main() {
fmt.Println("hello")
}If you’re using an IDE like VS Code with the Go extension, unused imports are usually highlighted. But doing this manually in a large file is tedious — the next two fixes automate it.
Fix 4: Use goimports to Auto-Fix Imports
goimports is the standard tool for managing Go imports. It adds missing imports and removes unused ones automatically.
Install it
go install golang.org/x/tools/cmd/goimports@latestRun it on a file
goimports -w main.goThe -w flag writes the changes back to the file. Without it, goimports prints the corrected code to stdout.
Run it on your entire project
goimports -w .Set it as your editor formatter
In VS Code, add this to your settings.json:
{
"go.formatTool": "goimports",
"editor.formatOnSave": true
}Now every time you save a .go file, unused imports are removed and missing imports are added automatically. This is the single best quality-of-life improvement for Go development. You’ll rarely see “imported and not used” errors again.
Pro Tip: If you’re debugging and want to temporarily keep an unused variable alive without the
_ = xtrick, wrap it in afmt.Println(x)call. This both “uses” the variable and gives you debug output. Just remember to remove it before committing.
goimports is a superset of gofmt — it does everything gofmt does (formatting) plus manages imports. There’s no reason not to use it. This kind of automatic tooling parallels how ESLint fixes parsing issues in JavaScript — letting tools handle mechanical code quality so you can focus on logic.
Fix 5: Use gopls Auto-Fix
gopls is the official Go language server. If you’re using VS Code, GoLand, or any editor with LSP support, gopls is likely already running. It provides code actions that can fix “declared and not used” and “imported and not used” errors automatically.
VS Code
- Place your cursor on the error (the red underline).
- Press
Ctrl+.(orCmd+.on macOS) to open the Quick Fix menu. - Select the appropriate fix — usually “Remove unused variable” or “Remove unused import.”
Command line
You can also run gopls fixes from the command line:
gopls fix -a main.goKeep gopls updated
go install golang.org/x/tools/gopls@latestAn outdated gopls can miss newer language features. If your editor is showing false errors, updating gopls often fixes it. This is analogous to keeping your Node.js tooling current when facing module resolution errors — stale tools cause stale problems.
Fix 6: Use _ for Unused Function Returns
Some functions return values that you genuinely don’t need. Go’s multiple return values make this common.
The error:
func main() {
file, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
// file declared and not used
}You opened the file and checked the error, but you haven’t used file yet. Maybe you’re about to write the code that reads from it, or maybe you only wanted to check if the file exists.
Fix — use the variable:
func main() {
file, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Now file is used
}Fix — discard the variable if you only need the error:
func main() {
_, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
}Fix — check existence without keeping the file handle:
func main() {
if _, err := os.Stat("config.json"); err != nil {
log.Fatal("config file missing:", err)
}
}The if initializer pattern (if x, err := ...; err != nil) is idiomatic Go. It scopes the variable to the if block, so you don’t end up with unused variables in the outer scope.
Fix 7: Build Tags Causing Unused Variables
Build tags (build constraints) can cause “declared and not used” errors that seem to appear and disappear depending on your platform.
The scenario
You have a variable that’s only used in platform-specific code:
package main
import "fmt"
func main() {
configPath := getConfigPath()
fmt.Println(configPath)
}And getConfigPath() is defined in platform-specific files:
//go:build linux
package main
func getConfigPath() string {
return "/etc/myapp/config.yaml"
}//go:build windows
package main
func getConfigPath() string {
return `C:\ProgramData\myapp\config.yaml`
}If you build on macOS, neither file is included, and getConfigPath() is undefined. This causes a compile error — not “declared and not used” directly, but a related issue where platform-specific code introduces missing references.
The fix
Make sure there’s a file for every platform you support, or provide a default:
//go:build !linux && !windows
package main
func getConfigPath() string {
return "./config.yaml"
}Build tags and unused imports
Build tags can also cause “imported and not used” errors. If a package is only used inside a build-tagged file, building for a different platform means the import appears in other files without a corresponding usage. The solution is the same: keep platform-specific imports inside the build-tagged files where they’re used, not in shared files.
This kind of platform-dependent compilation issue is conceptually similar to Go module resolution problems where the build environment determines what the compiler can see.
Fix 8: Conditional Compilation and Debug Variables
During development, you often want to temporarily keep a variable or import alive for debugging purposes without actually using it in production code paths.
The _ assignment trick
Assign the variable to the blank identifier to “use” it without doing anything:
func processData(data []byte) error {
debugInfo := analyzeData(data)
_ = debugInfo // TODO: remove before committing
// ... rest of function
return nil
}This compiles and makes your intention clear: you plan to use debugInfo soon, you just haven’t written that code yet.
The var _ pattern for imports
If you need to import a package for its side effects (like database drivers or image format registration), use the blank import:
import (
"database/sql"
_ "github.com/lib/pq" // PostgreSQL driver -- imported for side effects
)The _ "package" syntax imports the package and runs its init() function without requiring you to reference any of its exports. This is the only correct way to import a package purely for its side effects.
Keeping an import alive temporarily
If you want to keep an import while you’re developing but you’re not using it yet:
import "encoding/json"
var _ = json.Marshal // Keep the import aliveThis references json.Marshal without calling it, so the import is “used.” Remove this line before committing. Some developers prefer the _ = json.Marshal line inside a function body, but the package-level var _ works too.
The //nolint approach (third-party linters only)
Note that //nolint comments do not work for these errors. declared and not used and imported and not used are compiler errors, not linter warnings. No comment directive can suppress them. Tools like golangci-lint have their own unused-variable checks that can be suppressed with //nolint, but those are separate from the compiler. The compiler error must be fixed by one of the methods described in this article.
Fix 9: Use the Variable in a Type Assertion or Interface Check
A common Go pattern is to verify at compile time that a type implements an interface. This uses the blank identifier with a type assertion:
var _ io.Reader = (*MyReader)(nil)This line declares nothing usable — it only checks that *MyReader satisfies io.Reader. If it doesn’t, you get a compile error at this line instead of a confusing error somewhere deep in your code.
This pattern is relevant because it shows the blank identifier used at the package level to “consume” a value without actually using it. The compiler sees _ as a valid usage target.
If you have an unused variable that you want to keep for a type check:
func handler(w http.ResponseWriter, r *http.Request) {
var rw http.ResponseWriter = w
_ = rw // If you only needed the type assertion
}But in most real code, you’d restructure to avoid the unnecessary variable entirely.
Fix 10: Short Variable Declarations in if/for Scopes
Variables declared in if and for statements are scoped to those blocks. This helps avoid “declared and not used” errors in the outer scope:
// Problem: err is unused in the outer scope if you only check it in the if block
err := doSomething()
if err != nil {
return err
}
// err is "used" here because the if statement reads it
// But if you had more complex logic, you might end up with unused varsBetter — use the if-init pattern:
if err := doSomething(); err != nil {
return err
}
// err doesn't exist here -- no chance of "declared and not used"For loops with unused loop variables:
// Go 1.22+ allows omitting unused loop variables
for range 10 {
fmt.Println("hello")
}
// Before Go 1.22, you needed the blank identifier
for _ = range make([]struct{}, 10) {
fmt.Println("hello")
}The if-init and for-range patterns are idiomatic Go. They keep variable scopes tight, which naturally prevents “declared and not used” errors and makes code easier to read.
Still Not Working?
Why this matters: Go’s compiler strictness about unused variables is a deliberate design choice, not a bug. Unused variables are a leading indicator of logic errors — a variable you thought you were using but aren’t often means a typo elsewhere, or a code path that doesn’t work as intended.
Shadowed variables
A common source of confusion is variable shadowing. You might think you’re using a variable, but you’re actually declaring a new one with the same name in an inner scope:
func loadConfig() (*Config, error) {
var cfg *Config
if useDefault {
cfg, err := defaultConfig() // This creates a NEW cfg in the if scope
if err != nil {
return nil, err
}
_ = cfg // The inner cfg is used here, but the outer cfg is never assigned
}
return cfg, nil // Outer cfg is still nil
}The := inside the if block creates a new cfg variable that shadows the outer one. The outer cfg is never used (or rather, never assigned a meaningful value). Fix this by using = instead of := when you want to assign to an existing variable:
if useDefault {
var err error
cfg, err = defaultConfig() // Assigns to the OUTER cfg
if err != nil {
return nil, err
}
}The go vet tool and gopls can detect shadowed variables. Run go vet ./... regularly.
Generated code with unused variables
If the error comes from generated code (protobuf, gRPC, ent, sqlc, etc.), don’t edit the generated file — it’ll be overwritten. Instead, update the code generator or the .proto/schema file. Most well-maintained generators produce code that compiles cleanly. If they don’t, file a bug with the generator.
Test files
Variables declared in _test.go files follow the same rules. If you have a test helper that declares a variable you’re not using in a specific test, the compiler will reject it. Use _ or restructure the test.
func TestSomething(t *testing.T) {
got, _ := functionUnderTest() // Discard the second return value if you're only testing the first
if got != expected {
t.Errorf("got %v, want %v", got, expected)
}
}Multiple errors compounding
Sometimes fixing one “declared and not used” error reveals another. This is because the compiler stops at the first few errors. After fixing the initial batch, rebuild to check for more. Running go build ./... compiles all packages in your module and shows all errors at once, rather than one package at a time.
The error appears only in CI
If the code compiles locally but fails in CI with “declared and not used,” check for differences in Go versions, build tags, or environment variables between your local machine and CI. A common cause is that CI runs go vet or staticcheck with stricter settings, or builds for a different GOOS/GOARCH that excludes platform-specific files. This parallels how module resolution can differ between environments when proxy or environment settings diverge.
Related: If you’re hitting module resolution issues in Go, see Fix: no required module provides package. For similar strictness in other languages, see Fix: TypeScript Type is not assignable to type, Fix: ESLint Parsing error: Unexpected token, and Fix: Python IndentationError: unexpected indent. For Node.js module problems, see Fix: Error Cannot find module (Node.js).
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: no required module provides package / cannot find package in Go
How to fix 'no required module provides package', 'cannot find package', and 'module not found' errors in Go. Covers go mod init, go mod tidy, go get, GOPATH vs Go modules, GO111MODULE, GOPROXY, GOPRIVATE, replace directives, vendor directory, go.work workspaces, and major version suffixes.
Fix: Go Module Not Found – cannot find module providing package
How to fix the Go error 'cannot find module providing package' caused by missing go.mod, wrong module path, private repos, or GOPATH issues.