Variables, types, constants
Go is statically typed but mostly invisibly so. The compiler infers types from
the right-hand side; you almost never write a type annotation. The walrus
operator := is the workhorse — declare and assign in one move.
Once you internalise zero values, half of Go's edge cases stop
surprising you.
1 · The intuition
Most languages with type inference still feel verbose because their syntax wants
a lot of words (const x: number = 42). Go's bet: pick the right
default operator, and 90% of declarations need no annotations at all.
There are two declaration forms you'll see. var x int = 42 is the
long form — explicit and rare. x := 42 is the short form — declare
and infer in one move. The short form is package-private to functions; you can't
use := at file scope. That's the only rule that catches newcomers.
int starts at 0, string
at "", bool at false, pointers at nil,
structs at the zero of every field. This sounds boring; it eliminates a whole
class of bugs.2 · Try it
package main
import "fmt"
func main() {
// Long form — explicit type
var name string = "Go"
// Long form with inferred type
var age = 15
// Short form — declare and infer, function-scope only
isAwesome := true
fmt.Printf("%s, age %d, awesome=%v
", name, age, isAwesome)
// Zero values — declare without initialising
var counter int
var greeting string
var flag bool
fmt.Printf("zero values: %d, %q, %v
", counter, greeting, flag)
}3 · The numeric types — and why there are so many
Go has explicit sized integer types: int8, int16,
int32, int64 and unsigned variants
uint8 … uint64. There's also a plain int
which is "the size of a register on your machine" — 64 bits on every modern
platform.
var byteVal byte = 255 // alias for uint8
var smallInt int16 = -30000
var bigInt int64 = 9_000_000_000 // shows for readability
var unsigned uint = 42 // platform-sized
var pi float64 = 3.14159 // most common float
var smaller float32 = 1.5 // half the precision
var ch rune = '⌘' // alias for int32; a Unicode code point
var letter byte = 'a' // alias for uint8; an ASCII byteUse int by default. Reach for sized integers when
memory layout matters (parsing a binary protocol) or interop demands it. Use
byte for raw bytes, rune for Unicode code points.
Don't mix the two — that's day 6 territory.
4 · Constants
Constants are like variables but the value is fixed at compile time. They have a quirk: they can be untyped, which makes the math more flexible.
const Pi = 3.14159 // untyped float constant
const greeting = "hello" // untyped string
const maxRetries int = 5 // typed int constant
// Untyped constants take the type of the context they're used in.
var radius float32 = 2
var area = Pi * radius * radius // Pi promotes to float32 here
fmt.Println(area) // 12.56636iota — the secret enum power tool
iota is a compile-time counter inside a const block.
It resets to 0 each block and increments per line. The classic use: enums.
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1 (iota auto-applies to subsequent lines)
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// Bit-flag enums use shifting
const (
ReadPerm = 1 << iota // 1
WritePerm // 2
ExecPerm // 4
)
var perms = ReadPerm | WritePerm // 35 · Type conversions — explicit always
Go does not auto-convert between numeric types. Even int and
int64 are different types; assigning one to the other without
a conversion is a compile error. The fix is a one-word cast.
var x int = 42
var y int64 = int64(x) // explicit conversion required
var z float64 = float64(x) // ditto
// Won't compile: var y int64 = x
// "cannot use x (variable of type int) as int64 value"
// Useful: convert byte slice to string and back
bs := []byte("hello")
s := string(bs)
fmt.Println(s, bs)uint32 that silently truncates to uint16, an
int that overflows when promoted to float64. Go's
rule: if a conversion can lose information, you write it. The compiler can't
be wrong about your intent if you stated it.6 · From the wild — real Go
Snippets from Kubernetes's API types. Notice — type aliases,
iota-style enums via untyped constants, and zero values being
meaningful (a zero NodeCondition means "unset").
// NodeConditionType is a valid value for NodeCondition.Type
type NodeConditionType string
// These are valid conditions of a node. Currently, we don't have enough
// information to decide node condition.
const (
NodeReady NodeConditionType = "Ready"
NodeMemoryPressure NodeConditionType = "MemoryPressure"
NodeDiskPressure NodeConditionType = "DiskPressure"
NodePIDPressure NodeConditionType = "PIDPressure"
NodeNetworkUnavailable NodeConditionType = "NetworkUnavailable"
)
// PodResourceClaimStatus is stored in the PodStatus for each PodResourceClaim
// which references a ResourceClaim or ResourceClaimTemplate.
type PodResourceClaimStatus struct {
// Name uniquely identifies this resource claim inside the pod.
Name string
// ResourceClaimName is the name of the ResourceClaim that was generated
// for the Pod in the namespace of the Pod. It this is unset, then generating
// a ResourceClaim was not necessary.
ResourceClaimName *string
}The named-string type (NodeConditionType) gives type safety:
you can't pass an arbitrary string where a NodeConditionType is
expected. The zero value of *string is nil, which means
"unset" — the API uses pointers specifically to distinguish "explicitly empty"
from "not provided".
7 · Coming from another language?
| If you know… | What's familiar | What surprises |
|---|---|---|
| Python | := looks like Python 3.8's walrus. Type-inference feels like dynamic typing. | Types are real at runtime — you cannot pass a string where an int is expected. No duck typing. |
| Java / C# | Static typing. Multiple integer widths. Constants. | No type annotation needed for most declarations. No final keyword — every variable is mutable; that's what const is for. |
| JavaScript / TypeScript | := ≈ const. Type inference. The walrus. | No let vs const distinction — every Go variable is mutable. const in Go means "compile-time constant", not "can't reassign". |
| Rust | Numeric types with explicit widths. Strong inference. | No ownership / borrowing — variables are just values. Untyped constants are more flexible than Rust's const. |
| C / C++ | Sized integers, explicit conversions, zero-as-uninitialised. | No pointer arithmetic. Strings are real (length-prefixed, immutable). The compiler is much louder about unused things. |
8 · Exercises (~10 min)
- Type spy. Use
fmt.Printf("%T\n", x)on each of:42,3.14,"go",'⌘',true. Predict each type before running. Anything surprise you? - The untyped constant. Write
const Pi = 3.14. Use it as afloat32in one expression andfloat64in another. Notice — it adapts. Now writeconst Pi float64 = 3.14and try the same. What error does the compiler give you? - The shadow trap. Inside a function: declare
x := 1. Inside anif true { }block, writex := 2then printx. After the block, printxagain. Predict, then run. (This is the shadowing bug pattern.) - Make an enum. Define a
type Status intwith valuesPending,Active,Suspended,Cancelledusingiota. Print them — what do they show as? Hint: they're just integers unless you also write aString() stringmethod (covered on day 4).
9 · Common mistakes
- Using
:=at package level. Package-scope variables needvar x = 5. The walrus only works inside functions. - Shadowing with
:=. If even one variable on the left is new,:=declares it; existing names get re-assigned.x, err := foo()afterx := bar()creates a newerrbut re-usesx. If you writex, err := foo()in a nested scope where outerxexists, you get an inner shadowedx— a classic subtle bug. - Confusing untyped and typed constants.
const Pi = 3.14is untyped and flexes;const Pi float32 = 3.14is fixed atfloat32. The untyped form composes better in math. - Forgetting zero values are real.
var users map[string]Userdeclares a nil map, which panics on write. Useusers := map[string]User{}orusers := make(map[string]User)instead. - Naming variables
type,map,chan,len. These are built-ins, not keywords — you can shadow them but you shouldn't. Pick a different name.
10 · When it clicks
- You reach for
:=by reflex inside functions andvarby reflex at file scope. - You don't write type annotations unless the compiler complains — and you can guess when it will.
- You read
const ( Sunday = iota; Monday; Tuesday )and immediately know it's an enum. - You know that
var users map[string]Useris a footgun and reach formake.