This page is for beginners learning golang variable scope and go variable scope: where a name is legal to use, how that differs from “global variable” wording in Go, and how short declarations and blocks interact. It starts with a quick mental model, then lexical nesting, local and package-level scope, :=, shadowing, habits that keep programs clear, and common mistakes.
Go 1.24 on Linux.
Quick answer: what is variable scope in Go?
Scope is where a declared name refers to a variable in your program text. In Go, start from the nearest enclosing function, if / for / switch, or { ... }: that block usually defines where the name is valid; the compiler’s undefined: name errors mean you used the name outside that region.
Lexical scope: outer names, inner blocks, and the spec
Go uses lexical (static) block scope: inner regions of code can read variables from enclosing blocks, but declarations inside an inner block are not visible after that block ends. If the compiler reports undefined: x, either x was never declared in scope or you are past the block where it lives. The rules are spelled out under Declarations and scope in the Go specification.
Inner code can use an outer binding; the same outer variable is updated when the inner assignment runs:
package main
import "fmt"
func main() {
n := 1
{
fmt.Println("inner read:", n)
n = 2
}
fmt.Println("after inner block:", n)
}You should see inner read: 1 then after inner block: 2. The inner { ... } does not declare a new n; it shares the outer n. If you instead wrote n := 2 inside the inner block, you would introduce a new inner n (shadowing)—covered later in this page.
Local and block scope
Variables declared inside a function
Names declared with var or := inside a function are local to that function (unless they appear in a nested block that limits them further). They are not visible to other functions in the package.
package main
import "fmt"
func add() {
x := 1
_ = x
}
func main() {
fmt.Println(x)
}Building this program fails with an error like undefined: x at the fmt.Println line, because x exists only inside add.
Variables in if, for, switch, and braced blocks
A variable declared in the if init statement is in scope for the whole if (including else if / else), but not after the if ends:
package main
import "fmt"
func main() {
if y := 2; y > 0 {
fmt.Println(y)
}
fmt.Println(y)
}A typical compiler error is undefined: y on the final line.
The index variable in a classic for loop is scoped to the loop body:
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
fmt.Println(i)
}
fmt.Println(i)
}Expect undefined: i after the loop.
Each { opens a new block. A name declared only inside an inner block is not visible after that block ends, even though the inner block can still see variables from enclosing blocks (shadowing is covered later).
package main
import "fmt"
func main() {
{
z := 3
fmt.Println(z)
}
fmt.Println(z)
}Building this fails with undefined: z on the last line, because z exists only inside the inner { ... }.
Package-level variables and global-style names
Names declared outside any function are package-level in Go. That covers var, const, type, and func declarations. Any .go file in the same package can use those names, subject to export rules.
package demo
var Count int // exported (starts with uppercase)
var retries int // unexported (lowercase)A second file in the same package can update Count with no import—same package, same namespace:
package demo
func Increment() {
Count++
}People often search for “golang global variables,” but Go has no global keyword. What many tutorials call a global variable is really a package-level variable: shared across every file in that package, not automatically visible in unrelated packages.
To use an exported name from a different package, import it and qualify the name (the string is your module path from go.mod—not copy-pastable unless that module exists):
package main
import "example.com/project/demo"
func main() {
demo.Count++
}The same export rule applies to variables, constants, functions, and types. For naming style, see Go naming conventions.
Package-level variables suit constants, shared configuration, registries, or small programs. Mutable package-level state gets harder to test and reason about as the program grows, and unsynchronized writes from multiple goroutines can race. Prefer function parameters and return values when a value really belongs to one call or object.
Short variable declaration scope (:=)
Inside functions, := declares and assigns with type inference. For a new variable with value 10, either var x = 10 or x := 10 is idiomatic inside the function body—you pick one declaration style, not both for the same name in the same block.
package main
import "fmt"
func main() {
var a = 10
b := 20
fmt.Println(a, b)
}You should see 10 and 20 on one line.
package main
x := 1
func main() {}You should see a syntax error such as non-declaration statement outside function body at the short declaration—use var x = 1 (or var x int) instead.
In a multi-name :=, at least one name on the left must be new in the current scope; otherwise use =. That rule is what sometimes surprises people when they think they are only assigning.
Variable shadowing
An inner declaration with the same name as an outer binding hides the outer one inside the inner block:
package main
import "fmt"
func main() {
x := "outer"
{
x := "inner"
fmt.Println(x)
}
fmt.Println(x)
}You should see inner then outer. The inner x is a different variable; assigning to the inner x does not change the outer x.
Shadowing with := in a small if or error-handling block is a common source of “wrong value” bugs. When in doubt, use a longer name (errOpen, pathResolved) or an extra line with var so intent stays obvious.
Practices and pitfalls
Keep each variable’s scope as small as practical: declare close to use, prefer function parameters over growing package-level mutable state, and use clearer names when the lifetime is long (package-level or long-lived structs).
Mistakes to avoid: using a local name outside its function or block; using := at package level; treating “visible in my package” as “global for the whole binary” when reasoning about other packages; accidental shadowing in if err := ... style code; reaching for package-level variables instead of explicit arguments when the dependency is really local to one call path.
Go variable scope cheat sheet
| Declaration location | Scope |
|---|---|
| Inside a function (no extra inner block) | Visible in that function body and its nested blocks |
Inside if / for / switch clause init |
Visible for that statement’s blocks only |
Inside { ... } |
Visible only inside those braces |
Outside any function (var, const, type, func) |
Package-level for all files in that package |
| Uppercase package-level name | Exported to other packages (qualified access) |
| Lowercase package-level name | Unexported—same package only |
:= |
Only inside functions |
| Inner redeclaration of same identifier | Shadows outer binding in the inner block |
References
- The Go Programming Language Specification — Declarations and scope
- Effective Go — Data (declaration and zero values context)
Summary
Golang scope is lexical: names are visible inside their block and nested blocks, and the compiler stops you from using them outside. Function-local and block-local variables are no longer in scope after the block ends; package-level var declarations live for the whole package, with export controlled by capitalization. Short declarations with := belong inside functions only. Shadowing means an inner x can hide an outer x—legal, but worth naming carefully. Keeping mutable state narrow (parameters and returns instead of wide package globals) usually makes Go programs easier to test and reason about, especially with concurrency.

