Types, Variables & Control Flow
Go's type system is simple but strict — learn the building blocks before you build anything real.
Go’s Type Philosophy
Go is statically typed — every variable has a type known at compile time. But unlike Java or C++, Go keeps its type system deliberately small. No generics chaos (until Go 1.18), no type hierarchies, no operator overloading.
Real-World Analogy
Go’s type system is like a well-organized toolbox. Every drawer is labeled. You can’t put a wrench in the screwdriver slot. It’s restrictive, but you never waste time searching — you always know exactly what you’re working with.
Basic Types
package main
import "fmt"
func main() {
// Integers
var age int = 30 // Platform-dependent size (usually 64-bit)
var port int16 = 8080 // Explicitly 16-bit
var userCount int64 = 1_000_000 // Underscores for readability
// Floating point
var price float64 = 29.99
var pi float32 = 3.14
// Boolean
var isActive bool = true
// String (immutable, UTF-8 encoded)
var name string = "Gopher"
// Byte and Rune
var initial byte = 'G' // alias for uint8
var emoji rune = '🚀' // alias for int32 (Unicode code point)
fmt.Println(age, port, userCount, price, pi, isActive, name, initial, emoji)
} Zero Values: Go’s Default Initialization
In Go, every variable is initialized to its zero value if you don’t assign one. No null, no undefined, no garbage memory.
var count int // 0
var total float64 // 0.0
var name string // "" (empty string)
var active bool // false
var data []byte // nil (slices, maps, pointers, channels) Why zero values matter in production:
Zero values eliminate an entire class of bugs. A string is always safe to use — it’s "", not null. An int is always 0, not random memory. Many Go types are designed so their zero value is useful: sync.Mutex{} is an unlocked mutex, bytes.Buffer{} is an empty buffer ready to write.
Declaring Variables
Go gives you multiple ways to declare variables, each with a purpose:
// 1. Full declaration (rarely used — verbose)
var name string = "Alice"
// 2. Type inference (Go figures out the type)
var name = "Alice" // inferred as string
// 3. Short declaration (most common inside functions)
name := "Alice" // := declares AND assigns
// 4. Multiple declarations
x, y := 10, 20
first, last := "John", "Doe"
// 5. Block declaration (for package-level vars)
var (
host = "localhost"
port = 8080
maxRetry = 3
) := only works inside functions. At the package level, you must use var. This is intentional — package-level variables should be obvious and explicit.
Constants
Constants are computed at compile time and never change:
const maxConnections = 100
const apiVersion = "v2"
const pi = 3.14159265358979
// Typed vs untyped constants
const typedMax int64 = 100 // Can only be used as int64
const untypedMax = 100 // Can be used as any numeric type
// iota: auto-incrementing constants (enums)
type Role int
const (
Admin Role = iota // 0
Editor // 1
Viewer // 2
)
// iota with bit shifting (permissions)
type Permission int
const (
Read Permission = 1 << iota // 1 (binary: 001)
Write // 2 (binary: 010)
Execute // 4 (binary: 100)
)
// Combine permissions with bitwise OR
userPerms := Read | Write // 3 (binary: 011) Real-World Analogy
iota is like numbered tickets at a bakery. The first customer gets 0, the next gets 1, and so on. But with bit shifting, each ticket represents a different power of 2 — like having separate on/off switches that can be combined.
Type Conversions
Go has no implicit type conversions. You must be explicit:
var i int = 42
var f float64 = float64(i) // Must convert explicitly
var u uint = uint(f) // Must convert explicitly
// String conversions
import "strconv"
numStr := strconv.Itoa(42) // int → string: "42"
num, err := strconv.Atoi("42") // string → int: 42
price, err := strconv.ParseFloat("29.99", 64) // string → float64
// This does NOT work the way you might expect:
s := string(65) // "A" (treats 65 as a rune/Unicode code point, NOT "65") If/Else
// Standard if/else
if age >= 18 {
fmt.Println("Adult")
} else if age >= 13 {
fmt.Println("Teenager")
} else {
fmt.Println("Child")
}
// If with initialization statement (idiomatic Go)
if err := doSomething(); err != nil {
// err is only in scope inside this if block
fmt.Println("Error:", err)
}
// err doesn't exist here — keeps scope tight The if err != nil pattern is Go’s most common idiom. You’ll see it hundreds of times in any Go codebase. The initialization form (if err := ...; err != nil) keeps the error variable scoped to where it’s handled, preventing accidental reuse.
For Loops (The Only Loop)
Go has one loop keyword: for. It replaces while, do-while, and for from other languages.
// Classic for loop
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// While-style loop
count := 0
for count < 10 {
count++
}
// Infinite loop
for {
// runs forever until break or return
if shouldStop() {
break
}
}
// Range over slice
fruits := []string{"apple", "banana", "cherry"}
for index, value := range fruits {
fmt.Printf("%d: %s\n", index, value)
}
// Range over map
ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
fmt.Printf("%s is %d\n", name, age)
}
// Skip index or value with _
for _, fruit := range fruits {
fmt.Println(fruit) // don't need the index
}
// Range over string (iterates runes, not bytes)
for i, ch := range "Hello 🌍" {
fmt.Printf("byte %d: %c\n", i, ch)
} Switch
Go’s switch is cleaner than most languages — no break needed, and it can match expressions:
// Basic switch (no break needed — Go doesn't fall through by default)
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Friday":
fmt.Println("Almost weekend")
case "Saturday", "Sunday": // Multiple values
fmt.Println("Weekend!")
default:
fmt.Println("Midweek")
}
// Switch with no condition (cleaner than if/else chains)
switch {
case temperature > 35:
fmt.Println("Too hot")
case temperature > 20:
fmt.Println("Nice")
case temperature > 0:
fmt.Println("Cold")
default:
fmt.Println("Freezing")
}
// Type switch (used with interfaces)
switch v := value.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
} Real-World Analogy
Go’s switch without a condition is like a bouncer checking a list of rules from top to bottom: “Are you on the VIP list? No. Are you over 21? No. Do you have a ticket? Yes — come in.” The first matching rule wins.
Slices: Go’s Dynamic Arrays
Arrays in Go have a fixed size. In practice, you almost always use slices — dynamic, flexible views over arrays.
// Creating slices
nums := []int{1, 2, 3, 4, 5} // Slice literal
names := make([]string, 0, 10) // Empty slice with capacity 10
// Appending (creates a new underlying array if capacity exceeded)
nums = append(nums, 6, 7)
names = append(names, "Alice", "Bob")
// Slicing (half-open interval: includes start, excludes end)
first3 := nums[0:3] // [1, 2, 3]
last2 := nums[len(nums)-2:] // [6, 7]
// Length vs Capacity
fmt.Println(len(nums)) // 7 (current elements)
fmt.Println(cap(nums)) // depends on growth strategy Maps: Key-Value Storage
// Creating maps
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Or with make
scores := make(map[string]int)
scores["math"] = 95
scores["physics"] = 88
// Reading (returns zero value if key doesn't exist)
age := ages["Alice"] // 30
unknown := ages["Charlie"] // 0 (zero value for int)
// Check if key exists (comma ok idiom)
age, exists := ages["Charlie"]
if !exists {
fmt.Println("Charlie not found")
}
// Delete
delete(ages, "Bob")
// Iterate (order is NOT guaranteed)
for name, age := range ages {
fmt.Printf("%s: %d\n", name, age)
} Maps are not safe for concurrent use. If multiple goroutines read and write to the same map, your program will crash with a fatal error. Use sync.Map or protect with a sync.RWMutex in concurrent code.
Key Takeaways
- Zero values eliminate null bugs — every type has a safe default (
0,"",false,nil) :=for short declarations inside functions,varfor package levelforis the only loop — it covers classic, while, infinite, and range iteration- Switch doesn’t fall through by default — no
breakneeded, multiple values per case - Slices over arrays — use
makefor pre-allocation,appendfor growing - Maps need the comma-ok idiom —
v, ok := m[key]to distinguish “not found” from “zero value” - No implicit type conversions —
float64(myInt)is required, Go won’t guess for you