Go Tour
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.goOutput:
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.goOutput:
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.goOutput:
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.goPossible 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.goOutput:
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.goOutput:
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. Userangefor slices; use the comma-ok form for maps. - Keep
append’s result. Do not index pastlen. - 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
- In
vars.go, add aconstfor the table count and use it in thePrintf. - In
orders.go, append four more items in a loop. Printlenandcapafter each append. Watch capacity jump. - In
ticket.go, reject table numbers greater than 20 with a new error. CallNewTicket(1, 21)frommainand print the result.