Skip to content

Fix: Go declared and not used / imported and not used (compile error)

FixDevs · (Updated: )

Part of:  Go, Rust & Systems Errors

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 used

Unused import:

./main.go:4:2: "fmt" imported and not used

Multiple unused variables:

./main.go:8:2: err declared and not used
./main.go:9:2: result declared and not used

Unused import with alias:

./main.go:5:2: "os" imported as os and not used

Both 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@latest

Run it on a file

goimports -w main.go

The -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 _ = x trick, wrap it in a fmt.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

  1. Place your cursor on the error (the red underline).
  2. Press Ctrl+. (or Cmd+. on macOS) to open the Quick Fix menu.
  3. 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.go

Keep gopls updated

go install golang.org/x/tools/gopls@latest

An outdated gopls can miss newer language features. If your editor is showing false errors, updating gopls often fixes it. Stale tools cause stale problems — pin tool versions in your project’s go.mod tool directive (Go 1.24+) or in a Makefile so every developer runs the same gopls.

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 alive

This 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 vars

Better — 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.

How other languages handle this

Go’s stance on unused code is the strictest among mainstream compiled languages. “Compile error, no exceptions, no flag” is a deliberate design choice — but it isn’t the only valid one. Knowing where each language sits on the strictness spectrum explains why developers coming from Java or JavaScript find Go’s behavior surprising.

Rust issues a warning (unused_variables, unused_imports) rather than an error. The code still compiles. You can silence individual warnings with #[allow(unused_variables)] or prefix the variable with an underscore (let _x = 42). Rust also has #[must_use] attributes that promote certain return values into warnings if they’re discarded, and you can promote any warning to an error with #![deny(unused_variables)] at the crate root. The default is permissive; the opt-in is strict.

TypeScript is off by default but configurable. Set "noUnusedLocals": true and "noUnusedParameters": true in tsconfig.json and the compiler reports unused declarations as errors (in strict mode) or warnings. Without those flags, unused code is silently allowed. ESLint’s no-unused-vars rule covers similar ground at the lint layer, and @typescript-eslint/no-unused-vars understands TypeScript-specific patterns like unused type parameters.

Java (javac) does not warn about unused local variables at all by default. You have to opt in via -Xlint:all or use an external tool like SpotBugs, Checkstyle, or your IDE (IntelliJ flags unused variables in the gutter, but the compiler itself ignores them). Java’s culture treats unused-variable warnings as IDE territory rather than compiler territory.

Kotlin issues warnings for unused parameters and local variables but never errors. The Kotlin compiler is designed to lean toward warnings rather than hard failures for stylistic issues. You can elevate warnings to errors with -Werror, which is the Kotlin equivalent of Rust’s deny(warnings).

C and C++ have no built-in unused-variable check in the language standard. GCC and Clang both offer -Wunused-variable and -Wunused-but-set-variable as opt-in warnings, and -Werror promotes them to errors. Most production C/C++ projects enable these warnings, but the language itself is silent.

Swift emits a warning for unused variables and recommends _ for intentional discards, similar to Go. Swift’s compiler is stricter than Java’s but lighter than Go’s — the warning never blocks compilation.

OCaml and F# issue warnings (which -warn-error can promote to errors), with _ and ignore available for explicit discards. ML-family languages generally treat unused bindings as suspicious but compilable.

Erlang and Elixir are unusual: unused variables prefixed with _ are explicitly allowed and idiomatic for pattern matching ({ok, _ResponseBody}). Unused variables without the underscore prefix produce a compile warning.

Why Go chose hard errors: Go’s design favors readability of large codebases over developer convenience during prototyping. The Go team’s position is that any warning that you can ignore will eventually be ignored, so important checks should be errors. Unused-import errors specifically reduce the size of compiled binaries and shorten compile times across millions of lines of Google’s monorepo. Once you’re used to running goimports on save, the error rarely fires in practice.

The takeaway: if you find Go’s strictness annoying during exploratory coding, the cultural pattern is “use goimports and gopls as your editor’s save hook” — not “fight the compiler.” Languages with warning-based unused-variable detection require external tools (linters, IDEs, CI checks) to enforce the same discipline. Go bakes the enforcement into the compiler itself.

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. Module resolution and build constraints both depend on the environment, so the same source file can be valid on one machine and broken on another.

Generics and type parameters

Go 1.18+ generics introduced a subtle case: an unused type parameter on a function or method does not trigger “declared and not used.” But if you instantiate a generic function and bind the result to a variable you never read, you still get the standard error. The compiler treats type parameters as part of the type signature, not as local declarations. If you’re refactoring code to use generics and seeing surprising unused-variable errors, the problem is almost always at the call site, not in the generic declaration itself.

Embedded structs with unused fields

Anonymous fields (embedded structs) do not trigger “declared and not used” — they’re treated as struct fields rather than local variables. But if you declare a local variable of an embedded type and never call any method on it, the error fires. This catches people writing test helpers that construct embedded types for setup and forget to actually use them. Either delete the construction or assign to _.

Closure captures

A variable captured by a closure counts as “used” for the purposes of the unused check, even if the closure is never called. This means you can have dead code that captures a variable just to keep it alive:

x := someValue
defer func() { _ = x }() // Keeps x alive without actually using it

This is technically valid Go, but staticcheck flags it as a smell. If you find yourself reaching for this pattern, the real fix is usually to remove the variable entirely.

Race detector and conditional compilation

When you build with -race, additional instrumentation code is generated, and some debug-only variables that are normally unused may appear used (because the race detector reads them). The error can flip between builds with and without -race. If a CI race build passes but the regular build fails (or vice versa), this is the most likely cause. Run both go build and go build -race locally to reproduce.


Related: Go’s unused-variable error pairs naturally with module resolution errors — both surface when the compiler’s view of your code disagrees with your editor’s. Fix module imports first, then deal with unused variables; doing them in the other order leaves you chasing phantom errors.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles