Lesson 03 / 18
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.
Published
In this lesson
- 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
Contents
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.
// 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.
// 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.
// 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.
// 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 = valueform 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
iotasequence, only two have a value written in the source; the other four are produced from the line’s owniotavalue combined with a repeated previous expression. iotais a single counter across an entireconstblock 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.