net/go.book
All Parts Marketing

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).

:= versus = — the single most common beginner typo
:= 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.

Integer division truncates silently
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 camelCase for unexported names (retryCount, maxAttempts) and PascalCase for 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 — i for a loop index, r for a reader, err for 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, not countInt; users []User, not userSlice.
  • Acronyms stay uniformly cased: userID, not userId; HTTPClient, not HttpClient — this matches how the standard library names things like http.Client and url.URL.

Short names are idiomatic only when the scope is short
A loop variable named 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)
}
  1. Run it with go run main.go and confirm 24.0 C reports as 75.2 F.
  2. Change celsius := 24.0 to celsius := 24 (dropping the decimal point) and run it again. It compiles and runs — celsius silently becomes an int, the formula's division truncates, and fmt.Printf's %.1f verb fed an int prints 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.
  3. Add a const boilingC float64 = 100 and print its Fahrenheit equivalent using the same formula.
  4. Bonus: rewrite the day-of-week iota example from this chapter and print int(Wednesday) to confirm it equals 3. If you've already read the next chapter's methods material, go further and add a String() string method so fmt.Println(Wednesday) prints "Wednesday" instead of 3 — 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.