Skip to content
beginner Phase 1 · Environment Setup and First Programs

Variables and Constants

Declare values with var and const, use short declaration, and understand zero values.

40m
0 problems
Topic Progress 0%

Every Variable Starts Somewhere: the Zero-Value Guarantee

In C, a local variable starts as whatever the last function left in that memory. Debugging that class of bug — reading a counter that was never initialized — is a rite of passage. Go removes the category: every declared variable is assigned its type's zero value before any code runs.

var count int          // 0
var name string        // ""
var ready bool         // false
var ratio float64      // 0
var pointer *User      // nil
var tags []string      // nil
var lookup map[string]int // nil
var handler func()     // nil

Two consequences follow. First, there is no uninitialized read — a variable you declared but did not assign still holds a defined value, which is why Go needs no null checks for ordinary scalars. Second, composite types are zeroed recursively: a struct's int fields are 0, its string fields are "", its pointer fields are nil, so a fresh var mu sync.Mutex is immediately usable. That property is what makes zero-value design a pattern rather than an aspiration — Nil Checks and Zero-Value Design builds the full habit, including which types should treat nil as ready rather than missing.

Go gives you four ways to declare, and they differ in scope, not in storage:

var total int                      // zero value, type explicit
var total2 = 10                    // type inferred: int
total3 := 20                       // short declaration — functions only
var subtotal int = 100             // explicit type, explicit value

The := form is not a different kind of variable; it is a scoping rule wearing syntax. It works only inside functions, it infers the type from the right-hand side, and at least one name on the left must be new — x, err := f() in a scope that already has an err reassigns that err and declares x, which is how the err := idiom keeps working across successive calls in one function. The same name, same type, same block means assignment; anything else means a new variable, usually by accident. That accident has a name — shadowing — and it bites hardest in conditionals, where the inner declaration hides the outer one for the rest of the block.

One more tool: the blank identifier. value, _ := m[key] discards the second result of the map lookup (the presence flag), and _ = err explicitly ignores a value the compiler would otherwise demand you use. Discarding is always deliberate in Go; nothing is thrown away implicitly.

declared and not used: The Error That Earns Its Keep

The compiler error every Go beginner meets in the first hour:

./main.go:9:6: count declared and not used

Delete the variable and the error moves to the next one. It feels pedantic until you understand what it prevents: a variable that was written and never read is nearly always a missing line of logic — the increment that never happened, the result that was computed into the wrong name — and reporting it at compile time beats discovering it in a stack trace. Combined with the unused-import rule from Your First Go Program, the build enforces a blunt invariant: everything declared in a function body participates in the program.

The rule has a boundary worth knowing precisely. It applies to local variables. Package-level declarations are exempt:

package cache

var debugMode = false   // legal: unused package-level var

func run() {
	debug := false       // illegal: unused local variable
	_ = debug            // the blank identifier silences it
}

A package-level variable is part of the package's state — another file in the same package can read it — so the compiler has no basis to call it dead. Locals have no such future: their scope is the function, and if the function never reads them, nothing will.

Scope itself is block-structured, and the rules are the same for var and :=:

var x = 1
{
	var x = 2   // new variable, inner block
	fmt.Println(x) // 2
}
fmt.Println(x) // 1 — the outer x is untouched

Every { } — a bare block, an if, a for, a switch case group — opens a scope where new declarations are possible and outer names can be hidden. Grouped declarations exist for related values, and they read better than five separate lines:

var (
	host    = "localhost"
	port    = 8080
	timeout = 30 * time.Second
)

Grouping is purely organizational; each variable still obeys the same initialization and scope rules. The discipline to build now: declare close to first use, keep the scope small enough that shadowing is visible, and reach for _ only when discarding a value is the actual intent — a habit that pays off directly in Conditionals with if and else, where short declarations inside conditions are the idiomatic shape.

const: Values the Compiler Knows Before the Program Runs

Some values are not configuration — they are facts: the number of milliseconds in a second, the protocol port, the retry limit. Declaring them const moves them from the runtime to the compiler, where misuse becomes a build error instead of a bug.

const (
	maxRetries  = 3
	defaultPort = 8080
	appName     = "orders"
)

Constants come in two flavors, and the untyped kind is more interesting than it looks. A literal like 3 is an untyped constant: an exact value with no size attached. It takes a type only when context demands one — assigned to an int variable it becomes int, passed to a float64 parameter it becomes float64, and i := 0 gives an int because the default type of an integer constant is int. That flexibility is why var x float64 = 1 compiles even though 1 looks integral: the constant 1 is exactly representable as a float, so the conversion is free and precise.

Untyped constants also carry arbitrary precision — the compiler evaluates them mathematically, with at least 256 bits behind integer constants, and nothing overflows until you force the value into a sized type:

const precise = 1 << 100          // fine: still a constant
var big int64 = 1 << 62           // fine: fits int64

var wrong int = 3.5               // compile error: 3.5 truncated to int
var alsoWrong int64 = 1 << 63     // compile error: overflows int64

This is the sharpest difference from C-family languages: Go refuses to silently truncate a constant, while the same statement in C quietly produces 3. The conversion happens only where you write it, which is the subject of Explicit Type Conversions — int(3.5) is also an error for a constant, because the compiler still sees a value it cannot represent.

A constant must be evaluable at compile time, which constrains its right-hand side to literals, other constants, and operators over them — no function calls, no reads of variables. That requirement is not a limitation so much as the point: constants can be used where the language demands fixed values, from array lengths (var buf [1024]byte needs a constant size) to case labels and struct tags. And because const x = y copies a value the compiler holds, changing a constant is a source change that forces every dependent file to recompile — exactly the blast radius you want for a value nothing may silently redefine.

iota: Enumerations Without an enum Keyword

Go has no enum keyword, and the replacement is a const block plus iota — a predeclared constant that starts at 0 in each const declaration and increments once per line.

const (
	Sunday    = iota // 0
	Monday           // 1
	Tuesday          // 2
	// ...
)

Each line without an expression repeats the previous line's expression, with iota bumped — so Monday and Tuesday are shorthand for = iota. The two facts to internalize: iota counts lines of the const block, not values you wrote, and it resets to 0 at the start of every new const block.

The pattern that makes iota earn its place is bit flags, where each constant owns one bit of a mask:

type Perm uint8

const (
	Read  Perm = 1 << iota // 1 << 0 = 1
	Write                 // 1 << 1 = 2
	Exec                  // 1 << 2 = 4
)

var p Perm = Read | Write

func Has(v Perm, flag Perm) bool { return v&flag != 0 }

Has(p, Read)   // true
Has(p, Exec)   // false

Read the block as a table being generated: line n gets 1 << n, and adding a permission in the middle shifts every later one — the compiler recomputes them all, so the values stay correct with no manual bookkeeping. Read|Write builds a mask by position, v&flag tests one bit, and clearing uses the bit-clear operator (v &^ Exec) — all covered as operators in Operators and Expressions.

Where iota needs discipline is readability. = iota with plain names reads fine; clever expressions like iota + 1 << 3 do not, and a reader should never have to simulate the arithmetic to learn a constant's value — print them once, or write the value in a comment when the expression is doing real work. Also remember the reset: two const blocks both start at 0, so copying a block without noticing starts your numbering over.

Putting the phase together: var for anything that changes, := for function-local values whose type is obvious, const for values with one right answer, and iota when the right answer is a sequence. With declarations solid, the next tool verifies the result — gofmt for shape, go doc for documentation, go vet for the mistakes that compile — in Formatting and Inspection Tools.

Quiz

1. What is the zero value of a freshly declared `var s struct { n int; name string; next *Node }`, and why does it matter?

Question 1 options

2. Which of these is a compile error, and why?

Question 2 options

3. In `const ( Read = 1 << iota; Write; Exec )`, what are Write and Exec, and what does a second identical const block below start at?

Question 3 options

4. Why does `var x float64 = 1` compile while `var y int = 3.5` does not?

Question 4 options

Flashcards

Question

What are Go's zero values, and what problem do they remove?

Answer

Every declared variable starts at its type's zero: 0 for numbers, "" for strings, false for bools, nil for pointers/slices/maps/chans/funcs/interfaces, and structs with all fields recursively zeroed. They remove uninitialized-memory bugs — you can never read a variable holding whatever the stack previously contained.

Question

How do `var x = 5` and `x := 5` differ?

Answer

Both declare an int initialized to 5; the difference is where they work. := is restricted to function bodies, always infers the type, and requires at least one new name on the left — reusing an existing same-type name in the same block reassigns it. var works at package level, can be grouped, and lets you declare without initializing or state the type explicitly.

Question

Why does Go allow an unused package-level variable but reject an unused local?

Answer

A package-level variable is visible to every file in the package, so the compiler cannot prove it is dead; a local's scope is its function, and if the function never reads it, nothing will — which almost always means a missing line of logic. Use the blank identifier `_` when discarding a local on purpose.

Question

What is an untyped constant, and when does it get a type?

Answer

It is an exact value with no size attached (literals like 3, true, 'a'). It adopts a type from context — assignment, argument, or arithmetic with a typed operand — and falls back to a default (int, float64, string, bool, rune) in contexts like `i := 0`. Because it carries full precision, the compiler rejects rather than truncates values that do not fit the target type.

Question

How does iota behave across lines and across const blocks?

Answer

It starts at 0 on the first line of each const block and increments once per line — counting lines, not names — while lines without an expression repeat the previous expression with the new iota. Every new const block resets to 0, so two parallel blocks number independently; `1 << iota` turns that into 1, 2, 4, ... masks.

Revision Notes

Key Takeaways

  • 1. Zero values make every declaration safe to read immediately — structs and arrays are zeroed recursively
  • 2. := is a scoping construct, not a new kind of variable: functions only, type inferred, same-name reuse in one block means reassignment
  • 3. declared and not used fails only for locals, because package-level names are visible to sibling files
  • 4. Untyped constants are exact values with arbitrary precision; Go refuses silent truncation, so conversions must be written
  • 5. iota counts lines per const block starting at 0 and resets per block — `1 << iota` generates flag masks without magic numbers

Interview Tips

  • • "What is the zero value of a map? A slice?" — nil for both; then add the useful nuance: reading from a nil map is safe and returns the zero value, while writing to one panics — showing you know behavior, not just values
  • • "Why does Go error on unused variables?" — locals: catches missing logic at build time; then contrast with package-level vars to show you know the boundary and the reason (cross-file visibility)
  • • "Explain untyped constants" — exact values until context assigns a type, arbitrary precision until forced into a sized type; use `var x int = 3.5` as your concrete example of refused truncation
  • • "How would you model permissions?" — type Perm uint8 plus `1 << iota`, then name the operators: | to combine, & to test, &^ to clear; that chains directly into an operators question

Cheat Sheet

Variables and Constants

Declaration forms:

Form Where Type Note
var x int anywhere explicit zero value
var x = 5 anywhere inferred works at package level
x := 5 functions only inferred ≥1 new name required; reassigns same-name/same-type
const x = 5 anywhere untyped until used must be compile-time evaluable

Zero values:

Type Value
numbers 0
string ""
bool false
pointer, slice, map, chan, func, interface nil
struct, array fields/elements recursively zeroed

Unused rule: locals error (declared and not used); package-level vars allowed; imports always error; discard deliberately with _.

Code template:

const (
	Read  Perm = 1 << iota // 1
	Write                  // 2
	Exec                   // 4
)

var (                     // grouped package state
	host = "localhost"
	port = 8080
)

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading