Variables, Constants & Scope
Go offers several ways to introduce names, each with different scope rules and compile-time constraints.
Search across all documentation pages
Go offers several ways to introduce names, each with different scope rules and compile-time constraints.
Knowing which form to use keeps code readable and avoids subtle loop-closure bugs.
Variables hold runtime values; constants are fixed at compile time.
Scope is lexical: inner blocks can shadow outer names, but package-level names are visible across files in the same package.
Short declaration (:=) is the idiomatic choice inside functions, while var and const work at package level.
iota generates related constant sequences without manual numbering.
Quick-reference recipe card - copy-paste ready.
package main
import "fmt"
const Version = "1.0.0"
var defaultPort = 8080
func main() {
host := "localhost"
port := defaultPort
fmt.Println(host, port, Version)
}When to reach for this:
:= for locals inside functions when type inference is clear.var at package level or when you need an explicit zero value without assignment.const for values that must not change and for iota enumerations.package main
import "fmt"
const (
StatusPending = iota
StatusActive
StatusDone
)
var registry = map[string]int{}
func main() {
for _, name := range []string{"ada", "grace"} {
registry[name] = StatusPending
}
// Closure pitfall: capture loop variable by parameter, not by reference.
for _, name := range []string{"ada", "grace"} {
go greet(name)
}
fmt.Println(registry)
}
func greet(name string) {
fmt.Println("hello", name)
}What this demonstrates:
iota constants for ordered states without magic numbers.var for shared mutable state (use with care).:= inside main.name as a parameter to greet avoids the classic loop-variable closure bug.main in dependency order among packages.const values must be compile-time computable (literals, arithmetic on constants, iota).:= reuses names in the same block only when at least one name on the left is new.| Form | Where | Example | Notes |
|---|---|---|---|
var x T | package or function | var n int | Zero value if no initializer |
var x = v | package or function | var port = 8080 | Type inferred |
x := v | function only | host := "local" | At least one new name per block |
const x = v | package or function | const Max = 100 | Compile-time only |
const ( + iota | package or function | see Recipe | Resets per const block |
| Scope | Visible from | Shadowing |
|---|---|---|
| Package | All files in same package | Inner block can shadow package name |
| Function | Entire function body | := in inner block creates new local |
Block (if, for) | Block body only | if x := f(); x != nil limits x to if |
// iota with bit masks
const (
FlagRead = 1 << iota
FlagWrite
FlagExec
)
// Explicit type on first const pins type for the block
const (
KB int = 1 << (10 * iota)
MB
GB
)go func() { fmt.Println(i) }() captures one mutable i. Fix: pass i as a parameter or copy i := i inside the loop body (Go 1.22+ creates per-iteration vars in for range).:= - x := 1 inside an inner block hides outer x and can confuse readers. Fix: use distinct names or assign x = 1 when reusing the outer variable._ for intentional discards.:= at package level - Short declaration is illegal outside functions. Fix: use var or const at package scope.int8 can truncate silently in some conversions. Fix: use typed constants or explicit conversion with bounds checks.| Alternative | Use When | Don't Use When |
|---|---|---|
:= locals | Most function bodies | Package-level declarations |
var with zero value | You need the type's zero explicitly | A simple := with initializer is clearer |
const + iota | Enumerations and bit flags | Values must be computed at runtime |
| Function parameters over globals | Testing and clarity | Hot paths where profiling shows parameter cost matters (rare) |
var works at package and function scope and can declare without an initializer (zero value).
:= only works inside functions and requires an initializer while inferring type.
Yes, if at least one name on the left-hand side is new.
a, b := 1, 2 then a, c := 3, 4 reuses a and declares c.
iota resets to 0 at the start of each const ( block and increments by one for each constant specification line, including blank lines with _.
Constants are not storage; they are compile-time values inlined or used in type checking.
Assigning a const to a var materializes a runtime value.
Unused locals usually indicate dead code or incomplete refactors.
The compiler enforces removal to keep binaries and reviews clean.
if err := do(); err != nil {
// err exists only in this if/else chain
}The variable declared in the if initializer is scoped to the entire if statement including else branches.
If the value never changes, prefer const.
If you need a pointer, slice, or map that is mutable but the reference is fixed, var is appropriate.
You can shadow an import with a local variable of the same name in a block, which breaks access to the import inside that block.
Rename imports with import fmt2 "fmt" to avoid collisions.
Each iteration now creates its own loop variables, reducing closure capture bugs.
Still pass loop vars to goroutines explicitly when targeting older Go versions or for clarity.
Untyped constants flex across numeric types (const x = 1).
Typed constants (const x int64 = 1) enforce a single type in expressions.
Only compile-time representable values: strings, numbers, booleans, and derived constant expressions.
Structs, slices, and maps require var.
_ discards a value but still counts as a name on the left for redeclaration rules.
_, err := f() declares err if new in the block.
var gives you without an initializerStack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026