skip to content

In Go, what happens when a local variable is named `len` or matches an imported package name?

level: middleimportance: nice to knowfreq 36%

answer

  1. names resolve outward, first match wins
  2. builtins are declarations, not keywords
  3. imports are bound per file
  4. the innermost declaration always wins
  5. universe, package, file, block

basics

~20 s

Both compile. Go resolves names innermost-first through block, file, package and universe scopes, so a local declaration hides a builtin like len or an imported package name for the rest of that block, and later uses of the original meaning fail to compile.

solid answer

~50 s

Go resolves a name by searching outward through nested scopes: the current block, enclosing blocks, the file block, the package block, and finally the universe block that holds predeclared identifiers such as `len`, `cap`, `new`, `copy`, `nil`, `true` and the built-in types. None of those are keywords, so a local declaration may reuse the name. `len := 4` is legal, and from that point in the block `len(s)` no longer compiles because `len` names an int. Imported package names live in the *file* block, so `url := rawInput` in a function shadows the `url` from `import "net/url"` and makes `url.Parse` a compile error in that function. The file block also explains a related fact: an import is visible only in the file that declares it, while top-level functions, types and variables are in the package block and are visible in every file of the package.

code

go · 5 lines
go
len := 4 // legal: len is predeclared, not a keyword
s := []int{1, 2, 3}
fmt.Println(len) // prints: 4
// fmt.Println(len(s)) // would not compile: len is an int here
_ = s

go deeper

for a junior

Know that builtins like len and imported package names are ordinary identifiers you can accidentally reuse, and that the resulting compile error talks about your variable's type rather than the thing you shadowed.

for a middle

Be able to name the scope ladder — universe, package, file, then blocks — and place imports in the file block and top-level declarations in the package block. Explain why the innermost declaration always wins.

for a senior

Demonstrate the review habit: catch locals named url, path, time or max before they land, and know that neither the compiler nor a default vet run will flag them for you.

for a principal

Own the naming conventions that make this a non-issue at scale, and be able to justify why a lint rule for it is or is not worth the false positives your codebase would generate.

## The scope ladder Go resolves an identifier by looking outward through a fixed chain of scopes: 1. **The innermost block** you are in, then each enclosing block, out to the function body and its parameters. 2. **The file block** — the names bound by that file's import declarations. 3. **The package block** — every top-level constant, type, variable and function declared in *any* file of the package. 4. **The universe block** — the predeclared identifiers built into the language: `len`, `cap`, `append`, `copy`, `make`, `new`, `delete`, `min`, `max`, `clear`, `panic`, `recover`, `print`, the predeclared types (`int`, `string`, `error`, `any`, …), and the predeclared constants `true`, `false` and `nil`. The first match wins, and there is no way to reach past a match to the one behind it. That single rule produces both effects in the question. ## Shadowing a predeclared identifier Predeclared names are not keywords. Go's keyword list is short — `func`, `if`, `range`, `type` and so on — and `len` is not on it. So this compiles: ```go len := 4 s := []int{1, 2, 3} fmt.Println(len) // 4 // fmt.Println(len(s)) // would not compile: len is an int here ``` The same is true of `new`, `copy`, `min`, `max`, `cap`, and even `true` and `nil`. Shadowing them is legal and occasionally sensible in a narrow scope — `max` as a loop-local bound reads perfectly well — but it costs you the original meaning for the rest of the block, and it costs the next reader a double take. The common accidental cases are variables named `len`, `new`, `copy`, `max` and `min`; the last three became more likely once `min`, `max` and `clear` were added as builtins, since older code that used those as ordinary variable names now shadows something. ## Shadowing an imported package name An import binds a name in the file block. A local variable in a function is in a deeper block, so it wins: ```go import "net/url" func parse(raw string) { url := raw // shadows the package name for the rest of this function u, err := url.Parse(raw) // compile error: string has no method Parse _, _ = u, err } ``` The error message is about the *variable's* type, not about the import, which is why this occasionally confuses people reading the diagnostic. Frequent collisions in real code: `url`, `path`, `sort`, `time`, `bytes`, `json`, `user`, `log`, `context`. If you want both, rename the variable (`rawURL`, `p`, `t`), or give the import an alias so the two names differ. ## The file block is genuinely per-file The file block is the one scope level that does not span the package, and it has two visible consequences: - An import is usable only in the file that declares it. Two files in one package each need their own import line for the same package, and one file's alias is invisible to the other. - A shadow of a package name is confined to the block that declares it. Naming a variable `url` in one function does not affect the file's other functions, let alone the package's other files. By contrast, top-level declarations are in the package block, so a top-level `func url()` *would* be visible across every file of the package and would collide with an `import "net/url"` in any file that has both. ## What to do about it There is no compiler diagnostic and no default vet check for either case — both are legal Go — so this is a review-time and naming-convention concern: - Do not name locals after builtins you use in the same function. If you want a length, `n` or `count` reads better than `len` anyway. - Prefer descriptive names over package names for locals: `rawURL`, `srcPath`, `startTime`. This removes the collision and improves the code independently. - When a domain word genuinely matches a package name, alias the import at the point of use rather than contorting the local name. The deeper point is that Go's visibility story has two different axes that are easy to conflate: an initial capital letter controls what other *packages* may see, while the block ladder above controls which declaration a *name* resolves to right here. Shadowing is entirely the second axis.

  • Which scope holds an imported package name, and why does that matter?
    The file block. That is why an import is usable only in the file that declares it — two files in one package each need their own import line — and why aliasing an import in one file has no effect on the others. It also means a local variable that reuses the name shadows the package only within its own block.
  • Can you shadow `nil` or `true` in Go?
    Yes. `nil`, `true` and `false` are predeclared identifiers in the universe block, not keywords, so `true := false` compiles and makes the original constant unreachable in that block. It is legal, deeply confusing, and something reviewers should reject on sight.
  • How does this scope ladder relate to a name being exported?
    They are independent axes. The initial capital letter decides whether another package may refer to a top-level name at all; the block ladder decides which declaration a name resolves to at a given point in the code. A local variable can shadow an unexported and an exported name equally.

saying these in an interview costs you the question

  • Says len is a keyword and cannot be redeclared
  • Claims naming a local after a package is a compile error by itself
  • Thinks imports are visible in every file of the package
  • Believes the compiler prefers the builtin over a local of the same name
  • Confuses exported-name visibility with name resolution scope