Variables, Types, and Constants
Every program you will ever write, underneath all its cleverness, is just data being stored, read, and transformed — a value coming in from somewhere, sitting in memory for a while, and eventually going somewhere else. That sounds almost too simple to write a whole chapter about, and yet the specific rules a language uses to store and reshape that data quietly decide how many 2 a.m. bugs you'll spend your career chasing. Go made some unusually strict choices here, and this chapter is about understanding them well enough that they stop feeling like restrictions and start feeling like a safety net.
A variable in Go is like a labeled box with a fixed shape stamped on the side: once you build the box to hold integers, you can never quietly slip a string in through the lid. The shape is decided once, at compile time, and the compiler enforces it every time you reach into the box.
Declaring Variables: var and :=
Go gives you two everyday ways to introduce a variable. The first is the
var keyword, which works everywhere — inside functions, and at package
level, outside any function:
var count int
var name string = "gopher"
var isActive bool
The second is the short variable declaration, :=, which only works inside
a function body and infers the type from the right-hand side:
count := 0
name := "gopher"
isActive := false
Both forms above produce identical variables; := is simply shorter and is
by far the more common style inside function bodies. var remains
necessary in three situations: declaring a variable at package level
(:= cannot appear outside a function), declaring a variable without
initializing it, and declaring a variable whose type you want to state
explicitly rather than let Go infer (for example, var ratio float64 = 1,
which would otherwise infer int from the literal 1).
:= declares and assigns a new variable; = only assigns to a variable that already exists. Writing count = 0 for a variable that was never declared fails to compile with "undefined: count." Writing count := 0 a second time in the same scope for the same variable fails with "no new variables on left side of :=." Read the compiler error carefully — it names exactly which of the two mistakes you made.Zero Values
Here's a small design decision with an outsized effect on how much you'll
trust your own code. Unlike many languages, Go never leaves a declared
variable holding garbage or undefined. Every type has a well-defined
zero value that a variable holds the instant it is declared with no
explicit initializer: 0 for every numeric type, false for bool, ""
for string, and nil for pointers, slices, maps, channels, functions,
and interfaces.
var count int // 0
var price float64 // 0.0
var label string // ""
var ready bool // false
Go has no concept of an uninitialized variable — only a variable holding its type's zero value. This single design decision eliminates an entire category of bugs that plague languages where reading before writing is undefined behavior.
Basic Types
Go's built-in types are grouped into a few families:
| Category | Types | Notes |
|---|---|---|
| Integers | int, int8/16/32/64, uint8/16/32/64 |
int is 64-bit on modern platforms; prefer it unless you have a specific reason for a sized variant |
| Floating point | float32, float64 |
Prefer float64; it is what most math functions expect |
| Boolean | bool |
Only true/false — no truthy integers |
| String | string |
Immutable, UTF-8 encoded byte sequence |
| Byte and rune | byte (alias for uint8), rune (alias for int32) |
byte is a raw 8-bit value; rune is a Unicode code point |
Strings deserve a closer look, and a small story helps explain why. Go was
built at a company operating in essentially every language on Earth, so
its designers couldn't afford a string type that only worked comfortably
for English. That history is exactly why Go strings trip up newcomers
coming from languages where a string is an array of fixed-width
characters: a Go string is a read-only sequence of bytes, conventionally
holding UTF-8 text, not a neat row of one-character-per-slot boxes.
Indexing a string with s[0] gives you a byte, not a "character" — for
text that might contain multi-byte Unicode characters (accented letters,
emoji, non-Latin scripts), iterate with range instead, which correctly
decodes one rune (Unicode code point) at a time:
s := "héllo"
for i, r := range s {
fmt.Printf("index %d: %c (%d)\n", i, r, r)
}
Notice the index jumps by more than one when it passes the multi-byte é
— range is walking runes, not bytes, and reports each rune's starting
byte position.
Type Conversion: Go's Strictness
Many languages quietly convert between related numeric types — pass an
int where a float64 is expected and it just works, no questions asked.
Go refuses to be that agreeable. Every conversion between distinct named
types, even closely related numeric ones, must be written explicitly:
var whole int = 10
var precise float64 = float64(whole)
var backToInt int = int(precise)
Assigning an int directly to a float64 variable, or vice versa, is a
compile error: "cannot use whole (variable of type int) as float64 value in
variable declaration." The conversion syntax T(value) is required every
time, and truncates rather than rounds when converting a float to an
integer type — int(3.99) gives 3, not 4.
7 / 2 where both operands are int evaluates to 3, not 3.5 — Go performs integer division and discards the remainder with no warning. To get a fractional result, convert at least one operand to a float first: float64(7) / float64(2) gives 3.5. Forgetting this is a frequent source of subtly wrong calculations that compile without complaint.This strictness extends to named types you define yourself. Given
type Celsius float64 and type Fahrenheit float64, a Celsius value
cannot be passed where a Fahrenheit is expected even though both are,
underneath, a float64 — you must convert explicitly. It feels fussy the
first time you hit it, but think about what it's actually protecting you
from: this is a deliberate trade, a little more typing at the call site,
in exchange for the compiler catching "I passed a temperature in the wrong
unit" as a type error instead of a runtime bug that quietly ships a
spacecraft into the wrong orbit — which is, almost verbatim, a mistake
that has actually happened in this industry.
Constants and iota
Constants are declared with const and must be computable at compile
time — no function calls, no reading a variable:
const Pi = 3.14159
const MaxRetries = 3
const AppName string = "netbook"
Unlike var, a const declared without an explicit type (like Pi and
MaxRetries above) is an untyped constant. It has no fixed type until
it is used in an expression that requires one, which lets the same
constant be used naturally as an int in one place and a float64 in
another without any conversion syntax — one of the few places Go relaxes
its usual strictness, precisely because the value is fixed and known at
compile time.
Go's iota identifier generates sequential constants inside a const
block, starting at 0 and incrementing with each line — the idiomatic way
to build enumerated constants:
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
Each subsequent line without its own explicit value repeats the same
expression (Weekday = iota) with iota incremented, so you rarely need
to write out every value by hand. iota also supports arithmetic, which is
how the standard library defines things like byte-size constants:
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
)
var, :=, and const at a Glance
| Form | Where it's valid | Type | Reassignable |
|---|---|---|---|
var x T |
Package level or inside a function | Explicit or zero value | Yes |
x := value |
Inside a function only | Inferred from value |
Yes |
const x = value |
Package level or inside a function | Untyped or explicit | No — compile error to reassign |
Naming Conventions
Go's style conventions for variable and constant names are unusually
consistent across the entire ecosystem, largely because gofmt and the
standard library set a strong example everyone follows — pick up any
open-source Go project and it will feel oddly familiar within minutes, a
rare thing across programming communities. A few rules worth
internalizing early:
- Use
camelCasefor unexported names (retryCount,maxAttempts) andPascalCasefor exported ones (RetryCount,MaxAttempts) — the same capitalization rule you'll meet again in the packages chapter, which also governs visibility. - Prefer short, scoped names for short-lived local variables —
ifor a loop index,rfor a reader,errfor an error — and reserve longer, descriptive names for identifiers with wider scope, like package-level variables or exported constants. - Avoid repeating type information already implied by the type system
itself:
count int, notcountInt;users []User, notuserSlice. - Acronyms stay uniformly cased:
userID, notuserId;HTTPClient, notHttpClient— this matches how the standard library names things likehttp.Clientandurl.URL.
i inside a five-line for loop is perfectly idiomatic Go. That same single-letter name for a struct field or a package-level variable read from a dozen different files becomes a real readability cost — the accepted convention scales the name's length to how far the reader has to carry its meaning in their head.Try It Yourself
Write a small program that converts a temperature from Celsius to Fahrenheit, deliberately exercising the conversion rules from this chapter:
package main
import "fmt"
func main() {
const freezingC float64 = 0
celsius := 24.0
fahrenheit := celsius*9/5 + 32
fmt.Printf("%.1f C is %.1f F\n", celsius, fahrenheit)
fmt.Printf("Freezing point: %.1f C is %.1f F\n",
freezingC, freezingC*9/5+32)
}
- Run it with
go run main.goand confirm24.0 Creports as75.2 F. - Change
celsius := 24.0tocelsius := 24(dropping the decimal point) and run it again. It compiles and runs —celsiussilently becomes anint, the formula's division truncates, andfmt.Printf's%.1fverb fed anintprints the tell-tale%!f(int=...)instead of a number. Explain in your own words why removing the decimal point changed the inferred type, and why the mismatch shows up at runtime instead of as a compile error. - Add a
const boilingC float64 = 100and print its Fahrenheit equivalent using the same formula. - Bonus: rewrite the day-of-week
iotaexample from this chapter and printint(Wednesday)to confirm it equals3. If you've already read the next chapter's methods material, go further and add aString() stringmethod sofmt.Println(Wednesday)prints"Wednesday"instead of3— otherwise skip that part for now and come back to it later.
A compiler that refuses to add an int to a float is not being pedantic — it is refusing to guess which one of you and the machine is wrong about the units.
Frequently Asked Questions
Why does var count int not just crash or hold garbage like it would in some other languages?
Because Go defines a zero value for every type and guarantees a freshly declared variable holds it immediately, with no gap where the variable exists but its contents are undefined. That single design decision is what makes var count int safe to read on the very next line — you get a real, predictable 0, not whatever bits happened to be sitting in memory beforehand.
I wrote celsius := 24 instead of 24.0 and the program still ran, just with wrong-looking output — what actually happened?
Dropping the decimal point changed what Go inferred for celsius: 24 infers as int, while 24.0 infers as float64. The untyped constants 9, 5, and 32 in the formula happily adopt int from context too, so the whole expression still compiles — it just now does integer division and produces an int result. The visible symptom shows up one step later, at the fmt.Printf call: %.1f expects a floating-point value, and handing it an int prints Go's format-mismatch marker, %!f(int=24), instead of a number. Nothing here is a compiler error; it is Go quietly inferring a different, internally consistent type for the whole expression, with the mismatch only surfacing once that value reaches a format verb that assumes float64.
Do I need to memorize every rule about byte versus rune before moving on?
Not fully — just remember the shape of the problem: a Go string is a sequence of bytes, len(s) counts bytes not characters, and range over a string walks runes (whole Unicode code points), not bytes. That's usually enough to avoid the classic trap of indexing a string with non-ASCII text and getting a stray byte instead of the character you expected; reach back for utf8.RuneCountInString and the full byte/rune breakdown once you actually hit that situation in real code.
Why does Go bother making named types like Celsius and Fahrenheit incompatible even though they're both just float64 underneath?
Because the whole point of giving them distinct names was to encode a meaning the compiler can enforce — if Go let them mix freely, the type names would be decoration rather than protection. It's a deliberate trade of a little extra typing at conversion sites for the compiler catching "wrong unit" mistakes as type errors instead of silent runtime bugs.
What's the practical difference between using iota and just writing out Sunday = 0, Monday = 1, ... by hand?
Functionally, nothing — both produce the same values. Practically, everything: if someone inserts Holiday between Wednesday and Thursday in a hand-numbered list, every day after it now has the wrong number unless someone remembers to renumber them all. iota ties each constant's value to its position in the block, so the compiler does that renumbering for you automatically, correctly, every time.
Where This Goes From Here
Notice the pattern running through this entire chapter: Go keeps refusing
to guess. It won't guess that your uninitialized variable meant to hold
0. It won't guess that your int was close enough to a float64. It
won't guess that a Celsius and a Fahrenheit are secretly
interchangeable just because they're both stored as floating-point
numbers underneath. Every one of those refusals is the same instinct
wearing a different outfit: make the machine ask, instead of silently
guessing wrong.
That instinct is about to show up again, in a more interesting shape. The next chapter is about functions — the tool Go gives you for turning a sequence of these strict, well-typed operations into a single, reusable, named unit of behavior. You've been declaring data. Next, you start teaching the language to do something with it.