./main.go:10:2: undefined: X

Quick answer

The compiler cannot resolve X in scope. This is always one of four things: a typo in the name, a missing import, an unexported identifier (starts with a lowercase letter) referenced from another package, or the defining file being excluded by a build tag for the build you're running. This is purely a compile-time failure — there is no runtime, no stack trace, only a file:line.

The exact error string

$ go build .
./main.go:10:2: undefined: greetUser
./handler.go:22:9: undefined: strings.Trimm

Go's build failures always carry a file:line:column prefix and stop the entire build — there is no partial binary, and no way to run code that references an unresolved name, even if that name is never actually called at runtime. This is a stricter guarantee than most dynamic languages offer: a Python or JavaScript program can run for hours before hitting an undefined name on a rarely-exercised code path, but Go's compiler resolves every identifier in every reachable file before producing anything at all.

How to read the message

The format is always undefined: <name> or undefined: <package>.<name>. The second form is the easier case: the package half already resolved (the import exists and is used), so the search space narrows to "does this exact member exist in that package" — almost always a typo of a real standard-library or third-party function name. The first form, a bare identifier, requires checking the four causes below in order, since the compiler gives no further hint about which one applies.

Cause 1: a typo in the identifier name

func greetUser(name string) string {
    return "Hello, " + name
}

func main() {
    fmt.Println(greetUsr("Ada"))   // ❌ undefined: greetUsr (typo)
}

// ✅ fix: match the exact spelling
fmt.Println(greetUser("Ada"))

The simplest and most common cause. Go's identifier resolution is exact-match only — there is no fuzzy matching or "did you mean" at the compiler level (though gopls-powered editors surface a suggestion as you type). A single dropped or transposed letter, especially in a long camelCase name, produces this error with no other symptom.

Cause 2: the package isn't imported (or isn't referenced with its prefix)

// ❌ no import for "strings" — every strings.* call is undefined
func clean(s string) string {
    return strings.TrimSpace(s)   // undefined: strings
}

// ✅ import the package the identifier lives in
import "strings"

func clean(s string) string {
    return strings.TrimSpace(s)
}

Go requires every package you reference to appear in the file's import block — there's no implicit global namespace the way some languages provide for their standard library. Copy-pasting a code snippet from documentation or another file without also copying its imports is the most common trigger; goimports (bundled with most Go tooling) can auto-add the missing import once you save, which is why editors configured with it rarely show this specific variant.

Cause 3: the identifier is unexported and you're calling it from another package

// file: internal/user/user.go, package user
func validateEmail(e string) bool { ... }   // lowercase = unexported

// file: main.go, package main
import "myapp/internal/user"

func main() {
    user.validateEmail("a@b.com")   // ❌ undefined: user.validateEmail
}

// ✅ export it by capitalizing the first letter, in its own package
func ValidateEmail(e string) bool { ... }
// then call it as user.ValidateEmail(...) from main.go

Go has no export or public keyword — visibility is determined entirely by the case of the first letter, a design choice that makes the export boundary visible directly in the name at every call site, with no need to check a separate declaration. An identifier starting with a lowercase letter is scoped to its own package only; referencing it with a package prefix from anywhere else fails to resolve, and the compiler reports it identically to a genuinely nonexistent name, since from the importing package's perspective, it effectively doesn't exist.

Cause 4: the defining file is excluded by a build tag for this build

// file: platform_linux.go
//go:build linux

package main
func platformInit() { ... }

// file: main.go — building on macOS or Windows:
func main() {
    platformInit()   // ❌ undefined: platformInit (file excluded on this OS)
}

// ✅ provide a version for every platform you build on, or a
//    fallback with a broader/negated constraint:
//go:build !linux
func platformInit() { /* no-op or generic fallback */ }

A //go:build constraint (or the older _linux.go / _windows.go / _test.go filename-suffix convention) tells the compiler to skip that entire file unless the stated condition matches the current GOOS/GOARCH or build mode. The function is textually present in your repository, your editor can even navigate to its definition, but the compiler for this specific build never parses that file at all — making this the least intuitive of the four causes, since nothing about the source code itself looks wrong.

Common variants at a glance

Message shapeLikely causeFix
undefined: myFunctypo, or the function is in an unimported packagecheck spelling; add the import
undefined: pkg.Funcpackage resolved, member name is wrongcheck the package's actual exported API
undefined: pkg.func (lowercase)calling an unexported identifier across packagesexport it (capitalize) or move the caller into that package
works in the IDE, fails on go buildbuild-tag-excluded file, or a different GOOS/GOARCH targetcheck //go:build constraints and filename suffixes

Why this happens: Go resolves names at compile time, exhaustively

Unlike a scripting language that resolves a name only when the line actually executes, Go's compiler performs identifier resolution across every file it decides to compile for the current build, before generating any code. This is a deliberate trade-off: it catches typos and missing imports in dead code paths that might never run in a dynamic language, at the cost of requiring every referenced name to genuinely exist and be visible before the program can run at all. The practical implication is that "undefined" is never a runtime surprise in Go the way a NameError can be in Python — it's always caught at the earliest possible point, which is also why the four causes above are exhaustive: there's no fifth category of "the name exists but something else went wrong," because name resolution is a single, total, compile-time pass.

Debugging checklist

Frequently Asked Questions

What does "undefined: X" mean in Go?

The compiler encountered an identifier — a function, variable, type, or constant — that it cannot resolve in the current scope or any imported package. This is strictly a compile-time failure: Go never gets far enough to produce a binary, so there is no stack trace, only a file:line pointing at the unresolved name.

Why does this happen even though the function is clearly defined somewhere in my project?

Most often the identifier is defined in a different package and either the import is missing, the package alias/prefix wasn't used when calling it, or the identifier starts with a lowercase letter and is therefore unexported — invisible outside its own package regardless of import. Go's compiler resolves names strictly per-package; being "defined somewhere in the module" is not the same as being visible from the file that references it.

How does Go decide whether an identifier is exported?

Purely by the first letter's case — there is no separate "export" keyword. An identifier starting with an uppercase letter (Func, Type, Var) is exported and visible to importing packages; one starting with lowercase (func, typo, helper) is unexported and only visible within its own package. A single accidental lowercase letter is one of the most common causes of this error when calling code you just wrote in another package.

Can build tags cause an "undefined" error even when the function exists in the file?

Yes. A build constraint comment (//go:build linux) or a _linux.go / _test.go filename suffix excludes that file from compilation under other conditions (a different OS, GOOS/GOARCH, or a normal non-test build). The function is textually present in your project but the compiler never even parses that file for the build you're running, so it reports the identifier as undefined.

Does gofmt or go vet catch this before I build?

No — gofmt only reformats syntax, and go vet checks for suspicious constructs in code that already compiles; neither performs full identifier resolution. An editor running gopls (the standard Go language server) does catch undefined identifiers live, as does go build/go run, which is ultimately the same identifier-resolution pass that produces this error.

Why does the error sometimes name a package, like undefined: strings.Trimm?

That format means the package itself resolved correctly (strings was imported and used), but the specific member you referenced inside it does not exist — almost always a typo of the real function name (strings.TrimSpace, not strings.Trimm). This is a narrower, easier case than a fully unresolved bare identifier, since the package half of the search already succeeded.

More Go, Rust & Java errors

Browse the full reference for Go, Rust, Java, and other backend language errors — exact message, cause, and fix.

All Error References Go: imported and not used HTTP Status Codes
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.