Why Does Go Force You to Use Every Import and Variable

Go requires all imports and variables to be used to prevent dead code and errors, fixable by assigning unused values to the blank identifier.

The compiler won't let you leave things on the table

You port a script from Python to Go. You add an import, write a function, realize you don't need the library, delete the function, and forget the import. In Python, the script runs. In Go, go build stops with imported and not used. You haven't even written the logic yet. This feels like a tantrum until you understand the design goal. Go treats unused code as a bug, not a draft.

The rule applies to variables too. Declare a variable and never read it, and the compiler rejects the program with declared and not used. This strictness is not arbitrary. It is a defense mechanism against dead code, bloat, and silent logic errors.

Why the rule exists

Go is a systems language that compiles to a single binary. Every dependency increases the attack surface, build time, and binary size. The compiler enforces usage to keep the dependency graph honest.

Unused imports often indicate copy-paste errors. You paste a block of code, forget to update a variable name, and the old variable sits there. In JavaScript, that variable is a silent memory leak waiting to happen. In Go, the compiler yells immediately. You fix the mistake before the code runs.

Unused variables signal broken logic. You load configuration, assign it to a variable, and then call a function that ignores it. Did you mean to pass the config? Is the function wrong? The error forces you to confront the gap in your data flow.

Think of it like packing a backpack for a hike. You grab a water bottle, a map, and a heavy rock. If you don't use the rock, you are carrying dead weight. Go forces you to drop the rock before you leave the trailhead. The compiler is a spell-checker for logic. It catches mistakes that other languages hide until runtime.

The blank identifier: your intentional discard

Go provides an escape hatch for cases where you must capture a value but do not need it. The blank identifier _ is a special symbol that discards a value. It is not a variable. You cannot read from it. It is a write-only sink that tells the compiler you acknowledged the value and chose to drop it.

Here is the simplest pattern: a function returns multiple values, and you only care about one.

package main

import "fmt"

// main demonstrates discarding an unwanted return value.
func main() {
    // someFunc returns two values.
    // Assigning the first to _ tells the compiler we saw it and ignored it intentionally.
    // Without _, the compiler rejects this with: assignment mismatch: 1 variable but someFunc returns 2 values.
    _, err := someFunc()
    if err != nil {
        fmt.Println("failed:", err)
    }
}

// someFunc returns a result and an error.
func someFunc() (int, error) {
    return 42, nil
}

The blank identifier works for imports too. When you write import _ "pkg", you are importing the package for its side effects only. The package name is not available in your code, but the import statement runs. This triggers the package's init function, which can register drivers or set up global state.

Convention aside: the community groups side-effect imports at the bottom of the import block, separated by a blank line. This visual distinction signals that the import is used for registration, not for calling functions.

Side-effect imports in the wild

Database drivers are the classic use case. The database/sql package provides a generic interface. Drivers register themselves by calling sql.Register inside their init function. You never call the driver package directly. You only call sql.Open with the driver name.

If you import the driver normally, the compiler complains because you never use the package name. The side-effect import solves this.

package main

import (
    "database/sql"
    // The underscore imports the package for its side effects only.
    // The package's init function runs, registering the driver with database/sql.
    // We never reference a symbol from pq directly in this file.
    _ "github.com/lib/pq"
)

func main() {
    // Open uses the driver registered by the side-effect import.
    // The string "postgres" matches the name registered by the driver's init function.
    db, err := sql.Open("postgres", "dbname=test")
    if err != nil {
        panic(err)
    }
    defer db.Close()
    // Discard db to satisfy the unused variable check in this minimal example.
    _ = db
}

The import is used to trigger initialization. The name is unused, but the import statement is active. This pattern is essential for plugins, drivers, and any package that registers itself globally.

Side-effect imports are a feature, not a hack. Use them for registration, not for hiding dependencies.

Variables and the flow of data

Unused variables are caught with the same rigor as imports. The compiler tracks every declaration and ensures it is read at least once. This rule applies to loop variables, function parameters, and local declarations.

If you declare a variable and never use it, the compiler rejects the program with declared and not used: x. This error often appears during refactoring. You add a variable to debug, print a value, then remove the print statement. The variable remains. The compiler forces you to clean up.

This behavior makes refactoring safe. You cannot leave zombie variables behind. Every symbol in your code has a purpose. If the purpose disappears, the symbol must go.

Scoped declarations help avoid unused variable errors. If a variable is only needed inside a conditional block, declare it there. The variable goes out of scope when the block ends, so the compiler does not check it outside the block.

func check() error {
    // err is declared in the if statement.
    // It is only visible inside the if block.
    // If the function returns, err is used.
    // If we fall through, err is out of scope and the compiler is happy.
    if err := validate(); err != nil {
        return err
    }
    return nil
}

func validate() error {
    return nil
}

If you declared err at the top of the function and only used it in the if block, the compiler would complain that err is unused in the success path. Scoping the declaration to the if statement solves this. This is the standard Go pattern for error handling.

Convention aside: if err := ... is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible and keeps variables tightly scoped. Do not hoist errors to the top of the function unless you need to reuse them.

Common pitfalls and workarounds

Build tags interact with the unused import rule. If you have a file with //go:build linux, and you import a Linux-only package, the import is valid on Linux. On macOS, the file is excluded from the build, so the import never appears. This works correctly.

However, if you import a package without a build tag in a file that has a build tag, the import is always included when the file is included. If the package is only needed on Linux, and you are building on macOS, the file is excluded, and the import is excluded. No error.

The error appears when you import a package that is not needed for the current build configuration. For example, you import a testing helper in a production file. The compiler rejects this with imported and not used. The fix is to move the import to a test file or add a build tag to the import line.

Another pitfall is the loop variable capture. In older versions of Go, loop variables were reused across iterations, which caused subtle bugs when capturing them in goroutines. Go 1.22 changed this so loop variables are created fresh for each iteration. If you declare a loop variable and never use it, the compiler rejects the program with loop variable i declared and not used. This error prevents accidental accumulation of unused state in loops.

The compiler is not your enemy. It is a spell-checker for logic. It catches mistakes that other languages hide until runtime.

Decision matrix

Use a blank identifier _ when a function returns multiple values and you only need some of them.

Use a side-effect import import _ "pkg" when a package registers itself via init and you never call its exported functions directly.

Use scoped variable declarations if err := ... when a variable is only needed inside a conditional block.

Remove the import or variable entirely when you are refactoring and the dependency is no longer needed.

Reach for build tags when an import is only valid for specific platforms or configurations.

Trust the compiler. Unused code is dead code. Keep your surface area small.

Where to go next