Fix

"declared and not used" Error in Go

Fix the 'declared and not used' error in Go by using the imported package or prefixing the import with a blank identifier.

The empty variable problem

You copy a working HTTP handler into a new file. You change the route, strip out the JSON marshaling, and run go build. The terminal stops you immediately. No runtime panic. No silent failure. Just a hard stop telling you that a variable or import was declared but never used.

This is Go's most famous compile-time guardrail. It catches dead code, forgotten copy-paste artifacts, and accidental logic gaps before the program ever runs. The error message is blunt, but the fix is straightforward. You either use the symbol, remove it, or tell the compiler you intentionally want to discard it.

Why Go refuses to compile unused code

Go trades convenience for explicitness. The language designers made a deliberate choice to fail fast when code contains unreachable declarations. Unused variables and imports are caught during the static analysis phase, which happens before any machine code is generated.

Think of it like a contractor reviewing blueprints before pouring concrete. If the plans show a door that leads nowhere, the contractor stops you. They do not guess whether you meant to build a closet, a window, or nothing at all. They force you to clarify the design. Go works the same way. The compiler tracks every name you introduce into a scope. If that name is never read, assigned to, or passed to another function, the build fails.

This rule applies to both local variables and package imports. It keeps codebases clean, reduces memory allocations for dead values, and makes refactoring safer. When you rename a function or remove a dependency, the compiler immediately shows you every file that still references the old name or imports the orphaned package.

Go does not guess your intent. It forces you to declare it.

The blank identifier in action

The language provides a special token for intentional discarding: the underscore _. It is called the blank identifier. You cannot read from it, assign to it, or pass it as a value. It exists solely to satisfy the compiler's requirement that every declared name must be used.

package main

import (
	"fmt"
	"strings"
)

// Main demonstrates the blank identifier with variables and imports.
func main() {
	// strings.Contains returns a bool and an error in some APIs.
	// We only care about the boolean result here.
	matched, _ := strings.Cut("hello world", " ")
	
	// fmt is imported to print the result.
	// The underscore discards the second return value intentionally.
	fmt.Println(matched)
}

The underscore tells the compiler that you saw the second return value, considered it, and decided it was irrelevant for this specific call site. The build succeeds because every declared name is now accounted for.

The blank identifier is a deliberate discard, not a magic eraser.

How the compiler tracks declarations

When you run go build, the compiler parses your source files into an abstract syntax tree. It then walks the tree to resolve names, check types, and verify usage. During this phase, it maintains a set of declared identifiers for each scope. Every time it encounters a variable declaration, function parameter, or import statement, it adds the name to the set.

As the compiler continues walking the tree, it marks names as used whenever they appear in expressions, assignments, or function calls. After the entire file is processed, it checks the set. Any name that remains unmarked triggers a compile error. The compiler rejects the file with declared and not used for variables, or imported and not used for packages.

This check happens before optimization or code generation. It is purely static. The compiler does not need to run your program to know that a variable was never referenced. This is why Go programs compile quickly and why dead code is virtually impossible to ship.

The underscore bypasses this check by design. It is a reserved token that the compiler recognizes as a valid identifier that is explicitly exempt from the usage requirement. You can use it multiple times in the same scope. Each use is independent. Assigning to _ does not store a value anywhere. It simply consumes the right-hand side of the expression and drops it.

Real-world patterns

The blank identifier appears frequently in production Go code, but usually in specific, well-understood contexts. The most common pattern is side-effect imports. Some packages do not export functions that you call directly. Instead, they register themselves with the standard library during program initialization.

Database drivers are the classic example. The database/sql package uses an interface-based registry. You import a driver package, and its init function calls sql.Register. You never reference the driver package by name in your code. The import exists solely to trigger that registration.

package main

import (
	"database/sql"
	_ "github.com/lib/pq" // Register the PostgreSQL driver with sql.Open
	"log"
)

// Connect opens a database connection using the registered driver.
func Connect(dsn string) (*sql.DB, error) {
	// sql.Open looks up the driver by name in the registry.
	// The underscore import ensured the registration happened at startup.
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		return nil, err
	}
	return db, nil
}

Another common pattern is profiling and debugging endpoints. The net/http/pprof package registers HTTP handlers for CPU and memory profiles. You import it with an underscore, and the handlers become available on your server without any explicit routing code.

package main

import (
	"net/http"
	_ "net/http/pprof" // Register profiling endpoints under /debug/pprof
	"log"
)

// StartServer runs a basic HTTP server with profiling enabled.
func StartServer(addr string) {
	// The pprof handlers are automatically available.
	// No explicit route registration is needed in this file.
	log.Printf("Server listening on %s", addr)
	http.ListenAndServe(addr, nil)
}

Side-effect imports are hidden wiring. Keep them visible and documented.

Pitfalls and common mistakes

The blank identifier is simple, but it creates predictable traps. The first trap is discarding errors. Go's error handling convention is explicit. Functions return errors as the last value, and the standard pattern is if err != nil { return err }. Using _ to swallow an error hides failures and breaks the convention. The community treats discarded errors as a code smell. If you ignore an error, add a comment explaining why it is safe to do so.

The second trap is assuming _ preserves values. It does not. Each assignment to _ is independent. You cannot read from it later, and you cannot use it to pass a value between scopes.

package main

import "fmt"

// Main shows why the blank identifier cannot store values.
func main() {
	// This assignment discards the value. It does not store it.
	_ = 42
	
	// This line fails to compile.
	// The compiler rejects it with: undefined: _
	// fmt.Println(_)
}

The third trap is confusing unused variables with unused imports. The compiler treats them separately. An unused variable triggers declared and not used. An unused import triggers imported and not used. The fix for both is the same: use the name, remove it, or replace it with _. But the underlying cause differs. Unused variables usually mean dead logic. Unused imports usually mean copy-paste leftovers or missing function calls.

When you see the error, check the line number. If it points to a variable declaration, trace the logic. If it points to an import block, check whether you removed a function call that relied on that package. Go's strictness is a feature. It catches mistakes that other languages silently ignore.

Discarding an error with an underscore is how silent failures start.

When to use the blank identifier

Go gives you precise tools for precise situations. Choose the right one based on what your code actually needs.

Use a regular variable when you need to store a value and reference it later in the same scope. Use the blank identifier when a function returns multiple values and you only need one of them. Use a side-effect import when you need a package's init function to run without referencing its exported symbols. Use a regular import when you will call functions, access types, or read constants from the package. Remove the import entirely when you copied code and forgot to clean up the dependencies.

Explicit is better than implicit. The compiler is your first reviewer.

Where to go next