The compiler catches what you missed
You add a logger to your HTTP handler. You import the log package. You write the logic, test the endpoint, and see it works. You switch to using fmt for debugging instead, but you forget to remove the import. You hit save. The build fails.
The compiler rejects the file with log imported and not used. It feels like the compiler is being pedantic. It is not. It is saving you from shipping dead code, hidden dependencies, and typos. Go enforces a strict rule: every identifier you declare must be used. This rule applies to variables, imports, functions, and methods. If you declare something, you must use it. The compiler forces you to acknowledge every piece of code in your file.
The rule: everything must be used
Go requires usage for every declaration within a package. This is a design choice that prioritizes code hygiene over permissiveness. Unused code is a liability. It confuses readers, hides bugs, and wastes resources. If a variable exists but is never read, it suggests a logic error. If an import exists but is never referenced, it suggests a dependency you no longer need. The compiler eliminates this noise automatically.
The rule has one major exception: exported names. If you define a function or type that starts with a capital letter, the compiler assumes other packages might use it. Exported identifiers are exempt from the "declared and not used" check. This allows libraries to define public APIs without triggering errors. Unexported names, however, must be used within the package. If you define a private helper function and never call it, the build fails.
This boundary is intentional. The compiler only checks the current package. It does not scan the entire module to see if another file calls your function. It trusts that exported names are part of the public contract. Unexported names are internal implementation details. If they are not used, they are dead code.
Minimal example: the unused variable
Consider a function that calculates a value but never returns it.
package main
import "fmt"
// CalculateSum adds two integers.
func CalculateSum(a, b int) {
// result is declared but never read or returned.
result := a + b
fmt.Println("Calculation done")
}
// Main runs the program.
func main() {
CalculateSum(1, 2)
}
The compiler rejects this program with result declared and not used. You allocated space for result, computed a value, and then discarded it. The compiler assumes this is a mistake. You either need to use result (perhaps by returning it) or remove the declaration. The error prevents silent logic errors where a computation happens but its output is lost.
How the compiler checks
When you run go build, the compiler parses your source files into an abstract syntax tree. It builds a symbol table that tracks every declaration. It records the name, type, and scope of each identifier. After parsing, the compiler performs a usage check. It walks the tree and marks every identifier that is referenced. At the end of the pass, it scans the symbol table. Any identifier that is declared but not marked as used triggers an error.
This check happens early in the compilation pipeline. It is a static analysis pass that does not require type checking or code generation. The compiler can reject the file before it even verifies that your types match. This makes the error fast and reliable. You get immediate feedback. The compiler does not run your code; it inspects the structure. If the structure contains unused declarations, the build stops.
Realistic example: side effects and blank imports
The most common trigger for this error is imports. You often need to import a package for its side effects without using any of its exported names. Database drivers, crypto providers, and template initializers register themselves with standard library packages during initialization. You need the import to run the init function, but you never call a function from the driver package directly.
package main
import (
"database/sql"
// _ imports the driver for its side effects only.
// The driver registers itself with database/sql during init.
_ "github.com/lib/pq"
)
// Connect opens a database connection.
func Connect() (*sql.DB, error) {
// sql.Open uses the driver registered by the blank import.
// The driver name "postgres" matches the registration.
return sql.Open("postgres", "dbname=test sslmode=disable")
}
The blank identifier _ tells the compiler to discard the package name. It imports the package, runs its initialization code, and then ignores the exported names. This is the standard pattern for side-effect imports. Without the blank identifier, the compiler would reject the file with imported and not used. The blank identifier is a deliberate signal: "I want the initialization, but I don't need the names."
Convention aside: The blank identifier is also used to discard return values. If a function returns multiple values and you only need one, you assign the others to _. result, _ := DoThing() says "I considered the second return value and chose to drop it." Use this sparingly with errors. Dropping an error without acknowledgment is a bug waiting to happen. The community prefers if err != nil { return err } because it makes the unhappy path visible.
The blank identifier deep dive
The blank identifier _ is a special variable in Go. It can be assigned to, but it can never be read. It is the only identifier that can be assigned multiple times in the same scope. It is the escape hatch for the "declared and not used" rule. When you assign a value to _, you are telling the compiler that you are aware of the value and you are intentionally ignoring it.
This is different from not assigning at all. If you forget to capture a return value, the compiler might complain about mismatched assignment counts. If you assign to _, you satisfy the assignment requirement and suppress the usage check. The blank identifier is a tool for explicit intent. It forces you to make a conscious decision to ignore a value. You cannot accidentally drop a value. You must write _ to do so.
Convention aside: Public names start with a capital letter. Private names start with a lowercase letter. There are no keywords like public or private. The compiler uses capitalization to determine visibility. Exported names are visible to other packages. Unexported names are visible only within the package. This convention is enforced by the compiler. If you try to access a lowercase name from another package, you get an undefined error.
Pitfalls and compiler messages
The "declared and not used" error appears in several forms. The compiler complains with imported and not used if you import a package and don't reference it. It rejects variable declared and not used for local variables. If you define a function and never call it, you get func declared and not used. If you define a method and never call it, you get the same error. The message varies slightly based on the identifier type, but the cause is the same.
Loop variables have special rules. In Go versions before 1.22, capturing loop variables in closures was a common trap. The loop variable was reused across iterations, so closures captured the same variable. This caused subtle bugs. Go 1.22 changed the semantics so that loop variables are created per iteration. The compiler now enforces this with a hard error if you try to capture a loop variable in a way that suggests the old behavior. The compiler rejects the program with loop variable i captured by func literal if you attempt a pattern that is no longer valid. This error prevents you from writing code that relies on the old, buggy semantics.
Another pitfall is method receivers. If you define a method on a type, the receiver name must be used or the method must be called. If you define a method that is never called and the receiver is not used within the method, the build fails. This ensures that every method has a purpose. Dead methods are errors.
Convention aside: The receiver name is usually one or two letters matching the type. (b *Buffer) Write(...) is standard. (this *Buffer) or (self *Buffer) are discouraged. The community expects short receiver names. gofmt does not enforce receiver naming, but reviewers will flag it. Trust the convention. Short names reduce noise.
Decision matrix
Use a blank identifier _ when you need a package's initialization side effects but don't reference its exported names. Use a blank identifier _ when a function returns multiple values and you only need one of them. Use a named variable when you intend to read or write the value later in the scope. Remove the declaration when the code is dead or the logic changed. Refactor the function signature when you are forced to ignore a return value that should be handled. Export the name when other packages need to use it. Keep the name unexported when it is an internal helper that must be used within the package.
Dead code is a lie. The compiler tells the truth.