Go Tour

Updated

September 13, 2026

Go Tour

A short walk through the pieces you will type every day: variables, if and for, a slice, a map, a struct, a function, a method, and an error return. Each listing is a complete program. The desk is the same; the files still run alone.

Mental model

Go is statically typed. Every name has a type at compile time. The zero value is usable: 0, "", nil, false. Control flow is explicit: if err != nil, a single for, no while. Composite data is a slice (sequence), a map (lookup), or a struct (named fields). Behaviour is functions, plus methods attached to a type. Failure is a value you return, not an exception you throw.

This chapter does not cover interfaces or generics. You do not need them to write the programs here.

Worked examples

Case 1: Variables and types

Save as vars.go. var names a type. := infers it from the right-hand side. Both are ordinary.

// vars.go
package main

import "fmt"

func main() {
    var tables int = 8
    open := true
    var title string
    price := 3.50

    fmt.Printf("tables=%d open=%t title=%q price=%.2f\n", tables, open, title, price)
}

Run:

go run vars.go

Output:

tables=8 open=true title="" price=3.50

title was never assigned, so it is "". int, bool, string, and float64 are the types you will see first. Printf verbs: %d integer, %t bool, %q quoted string, %.2f two decimal places.

Case 2: if and for

Save as shifts.go. There is one loop keyword. A three-clause for counts. range walks a slice.

// shifts.go
package main

import "fmt"

func main() {
    names := []string{"Amina", "Bo", "Cara"}
    for i := 0; i < len(names); i++ {
        if names[i] == "Bo" {
            fmt.Println("skip", names[i])
            continue
        }
        fmt.Println("shift", names[i])
    }
    for _, name := range names {
        fmt.Println("listed", name)
    }
}

Run:

go run shifts.go

Output:

shift Amina
skip Bo
shift Cara
listed Amina
listed Bo
listed Cara

continue skips the rest of that iteration. _ discards the index from range. There is no while; a condition-only for is the equivalent, shown in later chapters.

Case 3: A slice of orders

Save as orders.go. A slice is a view over an array: length, capacity, and a pointer you do not manage by hand. append may return a new backing array — always keep the result.

// orders.go
package main

import "fmt"

func main() {
    orders := []string{"tea"}
    orders = append(orders, "toast", "soup")
    fmt.Println("len", len(orders), "cap", cap(orders))
    fmt.Println("first", orders[0])
    fmt.Println("rest", orders[1:])
    for i, item := range orders {
        fmt.Printf("%d %s\n", i, item)
    }
}

Run:

go run orders.go

Output:

len 3 cap 3
first tea
rest [toast soup]
0 tea
1 toast
2 soup

Capacity can be larger than length after later appends. Do not index past len-1. orders[1:] is a new slice heading, same backing array.

Case 4: A map of tables

Save as tables.go. A map keys a value. The zero value of a missing key is usable. The comma-ok form tells presence apart from zero.

// tables.go
package main

import "fmt"

func main() {
    seats := map[int]string{1: "Amina", 2: "Bo"}
    seats[3] = "Cara"
    fmt.Println("table 2", seats[2])
    name, ok := seats[9]
    fmt.Printf("table 9 name=%q ok=%t\n", name, ok)
    delete(seats, 2)
    fmt.Println("len", len(seats))
    for table, who := range seats {
        fmt.Printf("%d:%s\n", table, who)
    }
}

Run:

go run tables.go

Possible output (range over a map is unordered):

table 2 Bo
table 9 name="" ok=false
len 2
1:Amina
3:Cara

The pair 1:Amina and 3:Cara may swap. Never depend on map iteration order.

Case 5: Struct, function, method, error

Save as ticket.go. A struct groups fields. A function returns (Ticket, error). A method has a receiver. Check err before using the value.

// ticket.go
package main

import (
    "fmt"
    "os"
)

type Ticket struct {
    ID    int
    Table int
}

func NewTicket(id, table int) (Ticket, error) {
    if id <= 0 {
        return Ticket{}, fmt.Errorf("id must be positive")
    }
    if table <= 0 {
        return Ticket{}, fmt.Errorf("table must be positive")
    }
    return Ticket{ID: id, Table: table}, nil
}

func (t Ticket) Label() string {
    return fmt.Sprintf("ticket %d → table %d", t.ID, t.Table)
}

func main() {
    t, err := NewTicket(12, 4)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(t.Label())

    _, err = NewTicket(0, 4)
    if err != nil {
        fmt.Println("rejected:", err)
    }
}

Run:

go run ticket.go

Output:

ticket 12 → table 4
rejected: id must be positive

nil means success. The empty Ticket{} on failure is the zero value; callers who skip the error check will see id 0, which is why the check is not optional.

The trap

Treating this tour as a pile of independent tricks, then inventing a mini-framework that wraps each one. The next program “improves” Case 5 by hiding the error and panicking. It is shorter. It is worse.

// panic_ticket.go
package main

import "fmt"

type Ticket struct {
    ID    int
    Table int
}

func MustTicket(id, table int) Ticket {
    if id <= 0 || table <= 0 {
        panic("bad ticket")
    }
    return Ticket{ID: id, Table: table}
}

func main() {
    t := MustTicket(12, 4)
    fmt.Printf("ticket %d → table %d\n", t.ID, t.Table)
}

Run:

go run panic_ticket.go

Output:

ticket 12 → table 4

Change 12 to 0 and the process dies. Must helpers belong in tests and main wiring, not in the function every caller uses. Return error. Check it.

The boring rule

  • Declare types you care about. Let := infer the rest in short functions.
  • One for. Use range for slices; use the comma-ok form for maps.
  • Keep append’s result. Do not index past len.
  • Maps are unordered. Presence is value, ok.
  • Structs hold data. Functions and methods do work. Errors come back as values.
  • Skip interfaces and generics until a second implementation or a real type parameter earns them.

Try this

  1. In vars.go, add a const for the table count and use it in the Printf.
  2. In orders.go, append four more items in a loop. Print len and cap after each append. Watch capacity jump.
  3. In ticket.go, reject table numbers greater than 20 with a new error. Call NewTicket(1, 21) from main and print the result.