Variables, Constants & Scope
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.
Summary
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.
Recipe
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:
- Use
:=for locals inside functions when type inference is clear. - Use
varat package level or when you need an explicit zero value without assignment. - Use
constfor values that must not change and foriotaenumerations. - Use block scope intentionally; avoid shadowing exported names accidentally.
Working Example
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:
iotaconstants for ordered states without magic numbers.- Package-level
varfor shared mutable state (use with care). - Function-local
:=insidemain. - Passing
nameas a parameter togreetavoids the classic loop-variable closure bug.
Deep Dive
How It Works
- Names are bound at compile time; unused locals and imports are errors.
- Package-level variables initialize before
mainin dependency order among packages. constvalues must be compile-time computable (literals, arithmetic on constants,iota).- Short variable declaration
:=reuses names in the same block only when at least one name on the left is new.
Declaration Forms at a Glance
| 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 and Shadowing
| 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 |
Go Notes
// 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
)Gotchas
- Loop variable in goroutine closures -
go func() { fmt.Println(i) }()captures one mutablei. Fix: passias a parameter or copyi := iinside the loop body (Go 1.22+ creates per-iteration vars infor range). - Shadowing with
:=-x := 1inside an inner block hides outerxand can confuse readers. Fix: use distinct names or assignx = 1when reusing the outer variable. - Unused variables - Locals declared but not read fail compilation. Fix: remove them or use
_for intentional discards. :=at package level - Short declaration is illegal outside functions. Fix: usevarorconstat package scope.- Untyped const overflow - Untyped constants adopt the type of the context; large values assigned to
int8can truncate silently in some conversions. Fix: use typed constants or explicit conversion with bounds checks. - Package-level mutable globals - Easy to create hidden coupling and race conditions. Fix: prefer passing dependencies explicitly or guard with sync primitives.
Alternatives
| 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) |
FAQs
What is the difference between var and :=?
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.
Can I redeclare a variable with := in the same block?
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.
How does iota reset?
iota resets to 0 at the start of each const ( block and increments by one for each constant specification line, including blank lines with _.
Are constants copied into variables at compile time?
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.
Why does Go forbid unused local variables?
Unused locals usually indicate dead code or incomplete refactors.
The compiler enforces removal to keep binaries and reviews clean.
What is block scope in if with initialization?
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.
Can package-level variables be constants instead?
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.
How do imported names interact with local shadowing?
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.
What changed in Go 1.22 for range loop variables?
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.
When should I use typed vs untyped constants?
Untyped constants flex across numeric types (const x = 1).
Typed constants (const x int64 = 1) enforce a single type in expressions.
Can constants be structs or slices?
Only compile-time representable values: strings, numbers, booleans, and derived constant expressions.
Structs, slices, and maps require var.
How do blank identifiers work with :=?
_ discards a value but still counts as a name on the left for redeclaration rules.
_, err := f() declares err if new in the block.
Related
- Zero Values and Initialization - What
vargives you without an initializer - Pointers & the Address-of Operator - Passing and mutating named values
- Go Fundamentals Basics - Quick tour of declaration snippets
- Go Fundamentals Best Practices - Naming and package-level state habits
Stack 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).