Variables & Constants
A program needs somewhere to put its data. In Go, that "somewhere" is a variable if it can change, or a constant if it can't. Go gives you a few different syntaxes for declaring each; this chapter walks through them and the small surprises that come with each one.
Learning objectives
- Declare variables with
varand the short form:=. - Know which form is allowed where (and why).
- Recall Go's zero values for every basic type.
- Use the blank identifier
_to discard values intentionally. - Define constants with
constand useiotafor enums. - Distinguish typed from untyped constants.
Declaring with var
The most explicit form. You name the variable, then optionally specify the type and/or an initial value:
package main
import "fmt"
func main() {
var name string // declared, no value (zero value: "")
var age int = 30 // type AND value
var country = "USA" // value only, type is inferred
var height float64 = 1.78
fmt.Println(name, age, country, height)
}
All four lines are valid. Most idiomatic Go uses the third form (no explicit type) when an initializer is present. Go can infer the type, and the explicit annotation just adds noise.
You can also declare multiple variables in a single var
block, which is common at the package level:
var (
appName = "greet"
appVersion = "1.0.0"
debug = false
)
Short declaration: :=
Inside a function, the most common way to declare and initialize a variable is the short form:
city := "San Francisco"
population := 883_305 // underscores allowed in numeric literals
The type is inferred from the right-hand side. := is
sweet and short, you'll use it constantly. Two rules:
- Function-scoped only. You can't use
:=at the package (top) level. Usevarthere. - At least one new name on the left. If all
variables on the left already exist, use
=to assign instead of:=.
x := 1 // declare
x = 2 // assign, already declared
x, y := 3, 4 // OK: y is new
y, z := 5, 6 // OK: z is new (y is reused)
y = 7 // OK: just assignment
// y, x := 8, 9 // ❌ both already declared: won't compile
_ (below) to silence the compiler
when you genuinely want to discard a value.
Multiple declarations
Both forms support multiple names at once:
var x, y, z int = 1, 2, 3
name, age := "John", 25
You can also swap two variables in one line, no temp variable needed:
a, b := 1, 2
a, b = b, a // a == 2, b == 1
fmt.Println(a, b)
Zero values
Unlike many languages, Go has no concept of "uninitialized" memory in user code. Every declared variable gets a sensible default, its zero value:
| Type | Zero value |
|---|---|
Numeric (int, float64, …) | 0 |
bool | false |
string | "" (empty) |
| Pointers, interfaces, slices, maps, channels, functions | nil |
| Structs | Each field zero-valued |
| Arrays | Each element zero-valued |
This means var x int is safe to use immediately, no
garbage value, no segfault. It's a small thing that prevents a
surprising number of bugs.
var (
n int
s string
ok bool
)
fmt.Printf("%d %q %t\n", n, s, ok) // prints: 0 "" false
The blank identifier _
Sometimes a function returns more values than you care about. Assign
the throwaways to _:
package main
import (
"fmt"
"strconv"
)
func main() {
n, _ := strconv.Atoi("42") // we don't care about the error here
fmt.Println(n)
}
You can also use _ to import a package purely for its
side effects (its init functions run; nothing else):
import _ "image/png" // registers the PNG decoder; we don't call it directly
_ on an error return is convenient, but it's a footgun
in production code. Get in the habit of handling errors (Chapter 23).
Reach for _ only when you genuinely don't need the value
, examples, throwaway scripts, or known-safe contexts.
Constants
A const is a value fixed at compile time. It can never be
reassigned:
const Pi = 3.14159
const Greeting = "Hello, Go!"
const MaxRetries = 3
// Pi = 3.14 // ❌ won't compile: cannot assign to Pi
Constants must be initialized at declaration. Their value must be a
compile-time constant expression: you can't say
const Now = time.Now(), because time.Now()
is a function call evaluated at run time.
You can group constants like variables:
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
iota: Go's constant generator
Inside a const ( … ) block, iota is a special
identifier that starts at 0 and increments by one for
every constant in the block. It's how Go does enums:
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
func main() {
today := Wednesday
fmt.Println("today is", today) // prints: today is 3
}
iota resets to 0 at the top of every
const block. You can use expressions:
const (
KB = 1 << (10 * (iota + 1)) // 1 << 10 == 1024
MB // 1 << 20
GB // 1 << 30
TB // 1 << 40
)
The iota pattern is everywhere in Go's standard library:
HTTP status codes, file open modes, log levels, you name it.
Typed vs untyped constants
A subtle but important detail. By default, constants are untyped:
const x = 100 // untyped numeric
var i int = x // OK, x adapts to int
var f float64 = x // OK: x adapts to float64
Untyped constants take on the type they need at the point of use. They're like flexible literals.
If you write const x int = 100, you've made x
a typed constant of type int. Now you
can't assign it to a float64 without an explicit
conversion:
const x int = 100
var f float64 = x // ❌ cannot use x (type int) as type float64
var f float64 = float64(x) // ✓ explicit conversion
For most everyday constants (numbers, strings), leave them untyped. You get more flexibility for free.
A peek at scope
Where you declare a variable determines where you can use it:
- Package-level declarations live outside any function and are visible everywhere in that package.
- Function-level declarations are visible only inside that function (and any nested blocks within it).
package main
import "fmt"
var greeting = "hi" // package-level
func main() {
name := "Alice" // function-level
if true {
place := "Mars" // block-level: only inside the if
fmt.Println(greeting, name, place)
}
// fmt.Println(place) // ❌ undefined: place
}
Chapter 17 has the full story on scope and the famous "shadowing" pitfall. For now, just notice that variables only exist in the block they're declared in.
Check your understanding
Practice exercises
Spot the zero values
Without running it, predict what this program prints. Then run it and check.
package main
import "fmt"
func main() {
var (
n int
f float64
s string
ok bool
list []int
)
fmt.Printf("%d %g %q %t %v\n", n, f, s, ok, list)
fmt.Println("list is nil?", list == nil)
}
Show expected output
0 0 "" false []
list is nil? true
Note that list prints as [] but is in
fact nil. We'll see why in Chapter 13: it's a quirk
of how slices print.
Build a tiny enum with iota
Define a Severity type with four constants:
Debug, Info, Warn,
Error using iota. Then write a function
that takes a Severity and returns a string label
(don't worry about implementing String() properly
yet, a simple switch is fine).
Show one possible solution
package main
import "fmt"
type Severity int
const (
Debug Severity = iota
Info
Warn
Error
)
func label(s Severity) string {
switch s {
case Debug:
return "DEBUG"
case Info:
return "INFO"
case Warn:
return "WARN"
case Error:
return "ERROR"
default:
return "UNKNOWN"
}
}
func main() {
levels := []Severity{Debug, Info, Warn, Error}
for _, l := range levels {
fmt.Println(label(l))
}
}
In Chapter 21 you'll learn to attach a String() method
to Severity so that fmt.Println(s) prints
the label automatically.
Further reading
- Go spec, Variables
- Go spec, Constants
- The Go Blog, Constants (deep dive on typed vs untyped)
- Go spec, Iota
Variables and constants down, onto types.