Golang Variable Scope: Local, Package-Level, Global, and Block Scope

Learn variable scope in Go, including local variables, package-level variables, global-style variables, block scope, short declarations, shadowing, and best practices.

Published

Updated

Read time 7 min read

Reviewed byDeepak Prasad

Golang Variable Scope: Local, Package-Level, Global, and Block Scope

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:

go
package main

import "fmt"

func main() {
	n := 1
	{
		fmt.Println("inner read:", n)
		n = 2
	}
	fmt.Println("after inner block:", n)
}
Output

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.

go
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:

go
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:

go
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).

go
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.

go
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:

go
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):

go
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.

go
package main

import "fmt"

func main() {
	var a = 10
	b := 20
	fmt.Println(a, b)
}
Output

You should see 10 and 20 on one line.

go
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:

go
package main

import "fmt"

func main() {
	x := "outer"
	{
		x := "inner"
		fmt.Println(x)
	}
	fmt.Println(x)
}
Output

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


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.


Frequently Asked Questions

1. What does variable scope mean in Go?

Scope is where a declared name is visible. Go uses lexical (static) block scope: inner blocks can see outer names, but not the other way around, and the compiler rejects uses outside the declaring block.

2. Does Go have global variables?

Go does not have a separate global keyword. Names declared outside any function are package-level: they are visible across all files in the same package. Other packages only see them if the name is exported (starts with an uppercase letter).

3. Can I use := outside a function?

No. Short variable declarations with := are only allowed inside functions. At package level use var (or const) declarations.

4. What is variable shadowing in Go?

Shadowing is when an inner declaration introduces the same identifier as an outer scope, hiding the outer binding inside the inner block. It is legal but easy to misread; prefer distinct names when it would confuse readers.

5. Are variables declared in an if init statement visible after the if?

No. Names declared in the if simple statement, including the init part of if init; condition, are scoped to the whole if/else chain for that if statement, not after it ends.

6. Why is my variable undefined outside the function?

Function-local names, including those from := inside the function, exist only in that function body (and nested blocks). Return values or parameters are how you pass data out.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …