The compiler sees a different world
You write fmt.Println("Hello"). You hit run. The terminal spits back undefined: fmt. You check the code. The line import "fmt" is right there, three lines up. You check the spelling. It is perfect. You restart your editor. You restart your computer. The error remains.
The compiler is not broken. You are looking at the source file, but the compiler is looking at the module graph, the package boundaries, and the visibility rules. The undefined error is the compiler's way of saying the identifier you used does not resolve to any declaration in the current scope, the imported packages, or the predeclared set. The disconnect between what you see and what the compiler resolves is the root of every undefined error.
Go is statically typed and explicitly linked. Every name must resolve before the binary is built. This strictness prevents runtime crashes from typos or missing dependencies. You pay the cost at compile time. The compiler performs a name resolution pass that walks up a strict hierarchy. If the name is missing at every level, the build fails with undefined: X.
Name resolution: how Go finds things
When the compiler encounters an identifier, it searches for a declaration in a specific order. It stops at the first match. If no match is found, the error fires.
The search starts in the innermost scope. It checks local variables, function parameters, and loop variables. If the name is not found, it moves to the package scope. It checks functions, types, and variables defined in the same package. If the name is still missing, it checks imported packages. It looks for a package named X and tries to access X.Y if you wrote X.Y. Finally, it checks the predeclared identifiers like int, string, nil, make, and len.
If the search exhausts all levels without a match, the compiler emits undefined: X. The error message tells you exactly which name failed. undefined: fmt means the package fmt is not in scope. undefined: fmt.Println means the package fmt exists, but the identifier Println does not exist within it.
Minimal example
The simplest case is a missing import. The compiler does not know about standard library packages unless you import them.
package main
func main() {
// The compiler rejects this with undefined: fmt.
// The identifier fmt has no declaration in this scope.
fmt.Println("Hello")
}
Adding the import resolves the error. The compiler loads the fmt package and finds Println.
package main
import "fmt"
func main() {
// fmt is now resolved to the imported package.
// Println is resolved to the exported function within fmt.
fmt.Println("Hello")
}
The compiler is a literalist. Feed it the exact name, or it stops.
Visibility and capitalization
Go does not use keywords like public or private. It uses capitalization. An identifier starting with a capital letter is exported. It is visible to other packages. An identifier starting with a lowercase letter is unexported. It is visible only within the same package.
This rule is the most common cause of undefined errors when working across files. You define a function in one file and try to call it from another. The compiler says the function is undefined. The function exists, but it is not exported.
// file: utils.go
package utils
// helper is unexported. Only code inside package utils can call this.
// The name starts with a lowercase letter.
func helper() {
// ...
}
// file: main.go
package main
import "myapp/utils"
func main() {
// This fails. helper is not exported from utils.
// The compiler reports undefined: utils.helper.
utils.helper()
}
Capitalization is the gatekeeper. Export or stay hidden.
To fix this, capitalize the name. The convention is to use PascalCase for exported names.
// file: utils.go
package utils
// Helper is exported. Other packages can access it.
func Helper() {
// ...
}
// file: main.go
package main
import "myapp/utils"
func main() {
// Helper is now resolved. The capital H makes it visible.
utils.Helper()
}
The module system and go.mod
Modern Go uses modules. The go.mod file defines the module path and its dependencies. The compiler relies on go.mod to resolve import paths. If you import a package that is not in go.mod, or if the module is not downloaded, the compiler cannot find the package. The result is undefined: package.
This often happens when you copy an import path from documentation but forget to add the dependency. The import statement is syntactically correct, but the module graph is incomplete.
package main
import "github.com/example/missing"
func main() {
// This fails if the module is not in go.mod or not downloaded.
// The compiler reports undefined: missing.
missing.DoThing()
}
The fix is to run go mod tidy. This command scans your code for imports, updates go.mod to include missing dependencies, and downloads them. It reconciles your imports with reality.
Trust go mod tidy. It fixes the gap between your code and the module graph.
Realistic example: package boundaries
In a real project, you split code across multiple files. Each file must declare a package. Files in the same directory usually share the same package declaration. If you mix package declarations, the compiler treats them as separate packages. Names defined in one package are undefined in the other unless exported.
Consider a web server. You have main.go and handlers.go. You want handlers.go to define a router function.
// file: main.go
package main
import (
"log"
"net/http"
"myapp/handlers"
)
func main() {
// handlers.NewRouter is defined in handlers.go.
// If handlers.go has package handlers, this works.
// If handlers.go has package main, this fails with undefined: handlers.
mux := handlers.NewRouter()
log.Fatal(http.ListenAndServe(":8080", mux))
}
// file: handlers/handlers.go
package handlers
import "net/http"
// NewRouter creates and returns a new router.
// The capital N exports this function.
func NewRouter() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
return mux
}
If handlers.go accidentally declares package main, the compiler treats it as part of main. The import myapp/handlers in main.go refers to a different directory or package. The name handlers becomes undefined. Check the package declaration at the top of every file.
Pitfalls and traps
Several subtle patterns trigger undefined errors.
Dot imports change the scope. When you use import . "fmt", the names from fmt are injected directly into the current scope. You can call Println without the prefix. If you then try to use fmt.Println, the compiler reports undefined: fmt. The package name itself is undefined because the dot import suppresses the package identifier.
package main
import . "fmt"
func main() {
// Println works because of the dot import.
Println("Hello")
// This fails. The dot import hides the package name.
// The compiler reports undefined: fmt.
fmt.Println("World")
}
Blank imports are for side effects. When you use import _ "driver", the package is initialized, but no names are imported. The package name is not available. Trying to use driver.Open results in undefined: driver.
package main
import _ "database/sql/driver"
func main() {
// This fails. The blank import discards the package name.
// The compiler reports undefined: driver.
driver.Open("dsn")
}
Build constraints can hide files. If a file has //go:build linux, it is only compiled on Linux. If you run the build on Windows, the file is ignored. Any names defined in that file are undefined. The error appears only on certain platforms.
// file: linux_only.go
//go:build linux
package main
func setup() {
// ...
}
// file: main.go
package main
func main() {
// This works on Linux.
// On Windows, the compiler reports undefined: setup.
setup()
}
IDE caching can cause false positives. Your editor's language server might show undefined errors even when the code compiles. This happens when the server's index is stale. Restart the language server or run go build to verify. The compiler is the source of truth.
Decision matrix
Use go mod tidy when the import path is correct but the module is missing from go.mod or the dependency is not downloaded. Run this command to synchronize your imports with the module graph.
Check capitalization when the package exists but the specific function, type, or variable is not accessible. Ensure the name starts with a capital letter if you are accessing it from a different package.
Verify the package declaration when multiple files are involved and names are missing across files. Ensure all files in the same directory share the same package declaration unless you intend to create separate packages.
Inspect build constraints when code compiles on one machine but fails with undefined errors on another. Look for //go:build tags that might exclude files based on the operating system or architecture.
Review import aliases when the package is imported but the standard name is rejected. If you use import h "net/http", you must use h.ListenAndServe, not http.ListenAndServe. The compiler reports undefined: http if you use the wrong alias.