---
title: 'Variables and Constants'
source: 'https://academia.sh/en/courses/go-fundamentals/variables-and-constants'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures how short declaration determines a value's type without ever writing it in the source"
    - 'Counts how many constants in an iota sequence have a value written in the source'
    - 'Runs proof of why the same short-declaration line is rejected at package level'
---

# Variables and Constants

Short declaration decides a variable's type without ever writing it into the source; only two of a six-constant iota sequence have a value written in the source — the other four come from repeating the previous expression — and the same short-declaration line is rejected at package level while it is accepted inside a function.

The previous lesson measured which parts are mandatory for a source to run, but throughout
`func main() {}` the body stayed empty — no question was ever asked about what was or was not
written into a value. This lesson steps into that gap. Do you have to write a variable's type
when defining it, where does short declaration infer the type it does not write, and who fills
in the in-between values of a sequential constant list?

The Programming Foundations course (M01/K02) built variable definition and binding using
Python; there, binding a name to a value happened with a single piece of syntax and the type
never appeared in the source at all. **That concept is not rebuilt here** — one sentence is
enough to state the difference: in Go a name **has a type**, and that type is either written or
inferred from a value; no variable is ever left typeless. What this lesson asks is where that
inference comes from.

## Where Short Declaration Infers the Type From

A variable is defined in Go in two forms: `var name type = value`, or short declaration,
`name := value`. In the second, the type is never written at all; the compiler determines the
variable's type by looking at the **type of the assigned value itself**. This is one of the
examples of "an unwritten thing having a defined counterpart" — the type was not written, but
a variable having no type is not an available option; the compiler **infers** that type from
the value.

We can ask what type the inference lands on at runtime using the `%T` format verb; this verb
prints the name of a value's **static** type, and that name is fixed at compile time — it gives
the same answer on every run.

```go
// types.go — type inferred by short declaration
package main

import "fmt"

func main() {
	integer := 42
	floatingPoint := 3.14
	str := "text"
	boolean := true

	fmt.Printf("%-14s %-8s value=%v\n", "integer", fmt.Sprintf("%T", integer), integer)
	fmt.Printf("%-14s %-8s value=%v\n", "floatingPoint", fmt.Sprintf("%T", floatingPoint), floatingPoint)
	fmt.Printf("%-14s %-8s value=%v\n", "str", fmt.Sprintf("%T", str), str)
	fmt.Printf("%-14s %-8s value=%v\n", "boolean", fmt.Sprintf("%T", boolean), boolean)

	var wideType float64 = 42
	fmt.Printf("%-14s %-8s value=%v\n", "wideType", fmt.Sprintf("%T", wideType), wideType)
}
```

```
integer        int      value=42
floatingPoint  float64  value=3.14
str            string   value=text
boolean        bool     value=true
wideType       float64  value=42
```

**LB7.** `%T` is a static name read from the source itself; it carries no machine-, version-,
or run-dependent information at all. This verb is what will be used throughout this lesson and
the rest of the course to read a type's name, because it never violates the ban on
environment-dependent data.

This shows a distinction that matters for the measurement: what `%T` prints is not a memory
address or the variable itself, it is the name of the **type information** the compiler
assigns to that variable. Running the same source in two different runs, `%T` always produces
the same string, just like `compiles` in the previous lesson always produced the same
`compiled`/`not compiled` answer. Every measurement tool in this course shares these two
properties: being deterministic and reading nothing that depends on the environment.

The first four lines show short declaration landing on four separate types from four separate
values: writing an integer gives `int`, writing a decimal gives `float64`, writing something in
quotes gives `string`, writing `true`/`false` gives `bool`. In none of them does the type name
appear in the source; the `:=` sign on the left of short declaration is shorthand for "infer
this name's type from the value on the right."

This inference works by looking at the assigned value's **written form**, not at some runtime
test. When `42` is written, the compiler sees while reading the source that this is an integer
constant; this decision is made without producing any value, without ever running the program.
This shows that short declaration is also a compile-time decision — only the type's name is
not written in the source, the decision itself is still made during compilation, before
runtime. Short declaration leaves nothing at all to runtime; what is unwritten is only the
**visible sequence of letters**, not the decision.

## How a Type Written With `var` Diverges

The fifth line takes a different path. If `42` had been written with short declaration it
would have been `int` — the first line already showed that. Here the same `42` is made
**explicitly** `float64` by the line `var wideType float64 = 42`. Where short declaration
infers the type from the value, `var` opens a path to setting the type by hand; the difference
between the two is **who decides**. In short declaration, the value decides; in `var`, the
writer decides.

This should not be confused with the implicit conversion rejection measured in the previous
lesson. `var wideType float64 = 42` is not a conversion, it is an **initial value assignment**
— `42` here is an untyped constant, and untyped constants take shape based on the target type.
A conversion comes into play when a **variable** whose type is already fixed is assigned to
another variable (`var a int32 = 1; var b int64 = a`). That difference will be measured again
later in the course; what needs to be noted here is that suppressing short declaration's result
with `var` is not a rejection, it is a **preference**.

`var` syntax has one more advantage: it can be used without an initial value. Writing
`var count int` defines `count` without giving it any value at all, and the compiler does not
reject this line — unlike short declaration, `var` does not demand a value. In this case the
value the variable takes is the **zero value**, the topic of a later lesson in this course;
here it is only worth noting that `var` allowing a type to be written and `var` working without
a value written come from the same root — both, unlike short declaration, do not depend on the
requirement of "inference from a value."

## Iota: Who Fills In the Unwritten Constant Values

Constants are defined with `const`, and when a group of them is listed one after another, a
special counter named `iota` can be used. `iota` starts at zero and increases by one on every
line of a `const` block; if a line has no expression of its own, the line repeats the previous
line's expression **as is**, and only the value of `iota` is updated.

This solves a real problem: numbering a set of states by hand (`Pending = 0`, `Running = 1`,
`Completed = 2`, ...) is both repetitive writing and requires every number to be shifted by
hand whenever a new state is inserted in between. `iota` ties this numbering to the line's
**order**; when a line is inserted in between, every value after it shifts automatically, none
of them is updated by hand. This is a mechanism built for state sets where the numbers
themselves do not matter, only their **order and uniqueness** matter.

```go
// constants.go — six constants with iota, how many values are written in the source
package main

import "fmt"

const (
	Pending  = iota
	Running
	Completed
	Failed  = iota * 100
	Cancelled
	TimedOut
)

func main() {
	fmt.Printf("%-12s %d\n", "Pending", Pending)
	fmt.Printf("%-12s %d\n", "Running", Running)
	fmt.Printf("%-12s %d\n", "Completed", Completed)
	fmt.Printf("%-12s %d\n", "Failed", Failed)
	fmt.Printf("%-12s %d\n", "Cancelled", Cancelled)
	fmt.Printf("%-12s %d\n", "TimedOut", TimedOut)
}
```

```
Pending      0
Running      1
Completed    2
Failed       300
Cancelled    400
TimedOut     500
```

**LB8.** Only **two** of the six constants (`Pending`, `Failed`) have an expression written in
the source; in four of them (`Running`, `Completed`, `Cancelled`, `TimedOut`) only a name sits
on the line, no value is ever written. The value of these four comes from two sources: the
`iota` value at that line's own position, and the expression **repeated** from the previous
line.

The table confirms this. `Running` and `Completed` repeat `Pending`'s expression (`iota`), but
are computed with the `iota` value of their own line (1 and 2 respectively) — that is why they
climb `0, 1, 2`. `Cancelled` and `TimedOut` follow the same logic, repeating `Failed`'s
expression (`iota * 100`), but multiplied by their own `iota` values (4 and 5) — climbing
`300, 400, 500`. `iota` is a **single counter** across the whole block; it does not reset when
a second expression appears in the group, it keeps increasing without a break, following line
order.

This gives the course's most indirect answer yet to the question "what stands in for the
unwritten." Nothing being written on a line does not mean that line gets a **random** or
**empty** value; two rules together — the next `iota` value and the repetition of the previous
expression — produce a fully defined result. All four unwritten values fall, for this reason,
into the `defined` class, not `open`: the result does not change from run to run, and a reader
of the source can reach the same numbers on paper too.

Since every constant here is used in a `fmt.Printf` call, we do not run into the unused
variable rejection; the rule measured in the first lesson still holds here — an unused
rejection never applies when a constant is defined at package level, because Go applies that
rejection only to **local** variables. A constant or variable defined at package level does not
stop compilation even if it is never read. This distinction will come up again in the next
section.

This single rule narrows the first lesson's rejection without contradicting it at all. The
"unused variable" rejection measured in the first lesson was the most commonly encountered of
the four rejections, and the question of which context it applies in was never asked there.
Here we see that the rejection is not universal: leaving a local variable unused is rejected,
leaving a package-level constant unused is not. There is a reason for this — a package-level
constant can be used somewhere entirely different, even in another file of the same package or
one exported to a different package altogether; the compiler, reading a single file, cannot
know whether that use will happen somewhere else later. A local variable, on the other hand, is
bound to its own function body, and the compiler can see in the same glance whether it is used
or not.

`iota` also has a way of being ignored: if the blank identifier `_` is written instead of a
name on a line, `iota` still increases, but no name is produced for that position. This is
another use of the blank identifier measured in the first lesson — there it was a way to pass
over an unused value **without naming it**; here it is a way to **skip** a number in the
sequence. In both cases the same word carries the same meaning: "I know this, but I will not
bind it to a name."

## Limiting Measurement: Short Declaration Does Not Work Everywhere

Short declaration is not as universal as it appears. To measure this, we return to the first
lesson's compiler call: we will try the same line in two different contexts and classify the
result.

```go
// compiler.go — helper that calls the compiler from inside the program
package main

import (
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classify(ok bool) string {
	if ok {
		return "compiled"
	}
	return "not compiled"
}
```

Try the same `x := 5` line in two different contexts: once outside a function, at
package level; once inside a function.

```go
// limiter.go — is short declaration rejected at package level
package main

import "fmt"

func main() {
	packageLevel := `package trial
x := 5
func F() { _ = x }`
	functionLevel := `package trial
func F() { x := 5; _ = x }`

	fmt.Printf("%-24s %s\n", "at package level", classify(compiles(packageLevel)))
	fmt.Printf("%-24s %s\n", "inside a function", classify(compiles(functionLevel)))
}
```

```
at package level         not compiled
inside a function        compiled
```

**LB9.** Both sources carry the same `x := 5` line; the only difference is whether that line
sits inside or outside a function body. The compiler only recognizes short declaration as a
valid **statement** inside a function body; at package level it expects a declaration header
like `var` or `const`, not `:=`.

This boundary shows why short declaration is not a "shortcut" but a **context-specific**
piece of syntax. Short declaration saves you from writing the word `var` and a type name over
and over while defining many variables one after another inside a function; at package level
there is no need for this shortcut, because package-level declarations are already mostly
written one at a time, with `var` or `const`, and their initialization order is resolved not
by file order but by dependency order — that resolution will come up again in the next lesson,
over basic types.

This rejection belongs to a different class than the four rejections in the first lesson.
There, the four rejected sources were syntactically valid; the compiler merely did not
**want** a particular pattern. Here `x := 5` is syntactically **invalid** at package level —
the language's grammar only recognizes the `:=` sign in a statement context, and package level
is not a statement context, it is a declaration list. This shows that the "rejected" class
itself has two subtypes: a pattern can be grammatically valid but the compiler **does not
want** it (the first lesson's four rejections), or the pattern itself is **never defined** in
that context (this lesson's rejection). Both produce the same `not compiled` answer, but one
is a rule violation, the other a grammar mismatch.

The practical consequence of this distinction is that the writing form has to **change** when
a value is meant to be defined at package level — moving from short declaration to `var` is not
a preference, it is a requirement. A line moved from inside a function to package level does
not just change its location; its writing form has to change too.

This lesson's three measurements — short declaration's type inference, `iota`'s filling of
constants, short declaration's rejection at package level — are three faces of the same
question. In all three, something is unwritten: a type name, four constant values, the
validity of a declaration form. In all three, the language does not leave the gap random: in
one it fills it by looking at a value, in one it fills it with a counter and a repetition rule,
in one it finds nothing to fill it with and rejects. The course's first two lessons measured
"what gets rejected" and "what is mandatory"; this lesson measures an in-between region that is
neither rejected nor mandatory — **inference**.

## Summary

- Short declaration (`:=`) infers a variable's type from the assigned value's own type,
  without ever writing it in the source.
- The `var name type = value` form lets the writer set the result of inference **by hand**;
  this is not a conversion, it is an initial value assignment.
- In a six-constant `iota` sequence, only two have a value written in the source; the other
  four are produced from the line's own `iota` value combined with a repeated previous
  expression.
- `iota` is a single counter across an entire `const` block and does not reset when a second
  expression appears.
- The same short-declaration line compiles inside a function body but is rejected at package
  level — short declaration is a valid statement only inside a function.

## Next Step

In this lesson a variable's type was always inferred from a **value** — `int` from `42`,
`float64` from `3.14`. But those types themselves were never questioned: what happens when an
integer overflows, what should be expected when two floating-point values are compared, and
what is the `rune` type actually a stand-in for? The next lesson takes up basic types by
measuring what each of them does in an unwritten case.
