The Comprehensive Reference & Tutorial

The Go Programming
Language

27 sections from first program to generics and concurrency — each section matched to a verified, runnable tutorial program.

Go 1.22 · Generics Goroutines · Channels · sync 27 Sections · 50+ Code Examples go_tutorial.go — run with go run Output shown for every major example
How to use this guide: Each section explains a concept, shows code, then shows the expected output. The matching lines in go_tutorial.go (at the bottom) produce exactly that output. Run go run go_tutorial.go and follow along.
I
Part I

Foundations — The Language & Core Types

§01

Introduction to Go

Go (often called Golang) was designed at Google in 2007 and open-sourced in 2009 by Robert Griesemer, Rob Pike, and Ken Thompson. It addresses the practical challenges of large-scale software: fast compilation, safe concurrency, and code that stays readable as teams grow.

What makes Go different

Compiled speed

Entire programs compile in seconds. Binaries are statically linked — one file, no runtime dependencies.

Built-in concurrency

Goroutines cost ~2 KB vs ~1 MB for OS threads. Channels make safe communication expressive.

Garbage collected

Memory is managed automatically with a low-latency GC. No malloc/free, no RAII.

Strongly typed

Static types with inference. No implicit conversion ever. Most bugs caught at compile time.

Minimal surface

Only 25 keywords. One formatting style enforced by gofmt. One right way to do most things.

Rich tooling

go fmt, go vet, go test, go doc — all built in, zero config.

The 25 keywords — all of them

go — complete keyword list
break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

Go vs other languages

FeatureGoPythonC++RustJava
CompilationFast compiledInterpretedSlow compiledSlow compiledJVM bytecode
Memory mgmtGCGCManual/RAIIOwnershipGC
ConcurrencyGoroutines+channelsasyncio/GILthreads/asyncasync/threadsthreads/virtual
Learning curveLow–mediumLowVery highHighMedium
DeploySingle static binaryInterpreter requiredShared libsStatic binaryJVM required
GenericsSince 1.18Duck typingTemplatesYesYes
§02

Installation & Setup

1
Install Go
bash
# macOS
brew install go

# Debian / Ubuntu / ChromeOS Linux
sudo apt install golang-go

# Windows
winget install GoLang.Go

# Or download: https://go.dev/dl/
go version   # → go version go1.22.x linux/amd64
2
Create a module and run
bash
mkdir ~/myapp && cd ~/myapp
go mod init github.com/yourname/myapp  # creates go.mod
# Write main.go, then:
go run main.go          # compile + run in one step
go build -o myapp       # compile to binary
./myapp                 # run the binary
3
Essential go commands
bash
go fmt ./...        # format all files (always run before commit)
go vet ./...        # static analysis — catches common bugs
go test ./...       # run all tests
go test -race ./... # run tests with race detector (important!)
go get pkg@v1.2.3   # add a dependency
go mod tidy         # remove unused dependencies
go doc fmt.Println  # show documentation for any symbol
go env              # print environment variables
Editor: VS Code + the official Go extension (golang.go) is the most popular setup. It provides IntelliSense, auto-import, format-on-save, inline type hints, and a full debugger — all powered by gopls.
§03

Hello, World!

gohello.go
// Every Go file starts with its package name
package main   // 'main' makes this an executable

// Import statements — group related packages
import (
    "fmt"       // formatted I/O
    "os"        // operating system interface
    "runtime"   // Go runtime information
)

// main() is the program's entry point — no params, no return
func main() {
    fmt.Println("Hello, World!")

    // Printf uses format verbs
    fmt.Printf("Go %s on %s/%s\n",
        runtime.Version(), runtime.GOOS, runtime.GOARCH)

    // Sprintf returns a formatted string
    msg := fmt.Sprintf("Running: %s", os.Args[0])
    fmt.Println(msg)
}
outputHello, World! Go go1.22.2 on linux/amd64 Running: /tmp/go-build.../hello

fmt format verbs — complete reference

VerbWhat it formatsExample output
%vDefault format for any type{Alice 30}
%+vStruct with field names{Name:Alice Age:30}
%#vGo syntax representationmain.Person{Name:"Alice"}
%TType of the valuemain.Person
%dInteger, decimal42
%bInteger, binary101010
%oInteger, octal52
%xInteger, hex lowercase2a
%08dZero-padded width 800000042
%-10dLeft-justified width 1042
%fFloat, decimal notation3.140000
%.2fFloat, 2 decimal places3.14
%eFloat, scientific notation3.14e+00
%gFloat, shortest representation3.14
%sString, unquotedhello
%qString, double-quoted"hello"
%cCharacter (rune)A
%tBooleantrue
%pPointer address0xc000012080
%wError wrap (only in fmt.Errorf)(wraps the error)
§04

Variables & Zero Values

Every variable in Go has a zero value — the default it holds when not explicitly initialised. There are no uninitialised variables, no undefined behaviour from reading an unset variable.

go
// var — explicit declaration (any scope)
var name string              // zero value: ""
var count int                // zero value: 0
var ratio float64            // zero value: 0.0
var active bool              // zero value: false
var ptr *int                 // zero value: nil

// var with initialiser — type inferred
var city = "London"
var pi   = 3.14159

// Short variable declaration — ONLY inside functions
x := 42          // declares x as int, assigns 42
y := "Gopher"    // declares y as string

// Multiple short declaration
a, b := 10, 20

// Swap — no temporary variable needed
a, b = b, a

// Blank identifier — discard unwanted values
for _, v := range []int{1, 2, 3} {
    fmt.Println(v)  // discard the index
}

// Package-level grouped declaration
var (
    MaxSize = 1024
    Debug   = false
    AppName = "myapp"
)
zero valuesint=0 float64=0 string="" bool=false *int=<nil>

Zero value quick reference

TypeZero ValueNotes
int, float64, etc.0All numeric types
string""Empty string, not nil
boolfalse
pointer, func, interfacenilSafe to compare with nil
slice, map, channilnil slice/chan usable; writing to nil map panics — use make()
structeach field zeroedRecursive zero values
array [N]T[N] zero valuesFully initialised, ready to use
Zero values are useful by design. A sync.Mutex is ready to lock without any initialisation call. A bytes.Buffer is ready to write to. A []int can be ranged over safely. Design your own types so the zero value is useful.
§05

Types

Numeric types

TypeSizeRange / ValueUse when
intPlatform (64-bit on 64-bit OS)−2⁶³ to 2⁶³−1General integer — default choice
int8 int16 int32 int641–8 bytesExplicit widthsSpecific size needed (network, files)
uint uint8 uint16 uint32 uint641–8 bytes0 to 2ⁿ−1Non-negative values only
float324 bytes~±3.4×10³⁸, 7 digitsGraphics, when space matters
float648 bytes~±1.8×10³⁰⁸, 15 digitsGeneral float — default choice
complex64/1288/16 bytes3+4iComplex math; use real(), imag()
byte1 byte0–255Alias for uint8; raw bytes
rune4 bytes0–1,114,111Alias for int32; Unicode code point
go — strings and runes
import "unicode/utf8"

s := "Hello, 世界 🐹"

// len() counts BYTES — not characters
fmt.Println(len(s))                          // 17

// utf8.RuneCountInString counts Unicode code points
fmt.Println(utf8.RuneCountInString(s))       // 11

// s[i] is a byte (uint8)
fmt.Printf("byte[0] = %d (%c)\n", s[0], s[0])   // 72 (H)

// Rune iteration: range decodes UTF-8 automatically
for i, r := range s {
    fmt.Printf("[%d] %c (U+%04X)\n", i, r, r)
}
// [0] H (U+0048)
// [1] e (U+0065)
// ...
// [7] 世 (U+4E16)   ← byte index 7, not character index 7
// [10] 界 (U+754C)  ← byte index 10

// Convert to []rune for random character access
runes := []rune(s)
fmt.Printf("rune[7] = %c\n", runes[7])      // 世

// strings.Builder — efficient concatenation (avoid + in loops)
var sb strings.Builder
for _, r := range s[:7] {
    fmt.Fprintf(&sb, "[%c]", r)
}
fmt.Println(sb.String())    // [H][e][l][l][o][,][ ]
output"Hello, 世界 🐹": bytes=17 runes=11 byte[0]=72 (H) rune[7]=19990 (世)
§06

Type Conversions & strconv

Go never converts types implicitly. Every conversion must be written explicitly. This eliminates entire classes of subtle bugs present in C, C++, and JavaScript.

go — numeric conversions
var i int    = 42
var f float64 = float64(i)     // int → float64 — must be explicit
var u uint   = uint(f * 1.5)   // float64 → uint — truncates toward zero

// This does NOT compile — implicit conversion forbidden:
// var x float64 = i   // ERROR: cannot use i (int) as float64

// String ↔ rune/byte
letter := string(65)              // "A"  (from Unicode code point)
bytes  := []byte("Hello")         // raw UTF-8 bytes: [72 101 108 108 111]
str    := string(bytes)           // back to "Hello"
go — strconv
import "strconv"

// int ↔ string
s := strconv.Itoa(255)           // "255"
n, err := strconv.Atoi("1024")  // 1024, nil

// Parse typed values
f64, _ := strconv.ParseFloat("3.14159", 64)
b, _   := strconv.ParseBool("true")
i64, _ := strconv.ParseInt("ff", 16, 64)  // base-16 → 255
u64, _ := strconv.ParseUint("42", 10, 64)

// Format to string
strconv.FormatFloat(math.Pi, 'f', 4, 64)  // "3.1416"
strconv.FormatInt(255, 16)                 // "ff"
strconv.FormatInt(255, 2)                  // "11111111"
strconv.FormatBool(true)                   // "true"

// Quote / unquote for safe string embedding
q  := strconv.Quote("say \"hello\"\tthere")
// "\"say \\\"hello\\\"\\tthere\""
uq, _ := strconv.Unquote(q)
outputItoa(255)="255" ParseFloat=3.14159 FormatInt(255,16)="ff" FormatInt(255,2)="11111111"
§07

Constants & iota

go
// Constants are evaluated at compile time
const Pi      = 3.14159265358979  // untyped — flexible with any numeric type
const AppName = "MyApp"
const MaxBuf  = 1 << 20           // 1 MiB — bit shift at compile time

// Typed constant
const Timeout time.Duration = 30 * time.Second

// ── iota — auto-incrementing in a const block ─────────────────
type Direction int
const (
    North Direction = iota  // 0
    East                     // 1
    South                    // 2
    West                     // 3
)

// Give it a String() method for readable output
func (d Direction) String() string {
    return [...]string{"North", "East", "South", "West"}[d]
}
fmt.Println(South)  // "South" (not 2)

// ── iota with expressions — ByteSize ─────────────────────────
type ByteSize float64
const (
    _           = iota                 // discard 0
    KB ByteSize = 1 << (10 * iota)    // 1024
    MB                                 // 1,048,576
    GB                                 // 1,073,741,824
    TB                                 // 1,099,511,627,776
)

// ── iota for bit flags ────────────────────────────────────────
type Permission uint
const (
    Read    Permission = 1 << iota  // 001 = 1
    Write                            // 010 = 2
    Execute                          // 100 = 4
)

perms := Read | Write                   // 011 = 3
fmt.Printf("%b\n", perms)              // 11
fmt.Printf("has Execute: %t\n", perms&Execute != 0)  // false
fmt.Printf("has Read:    %t\n", perms&Read    != 0)  // true
outputSouth 11 has Execute: false has Read: true
Untyped constants are flexible. const Pi = 3.14159 works in expressions with float32, float64, or complex128 — without any cast. A typed constant like const KB ByteSize = 1024 only works where ByteSize is expected.
II
Part II

Control Flow & Collections

§08

Control Flow — if, for, switch

if — with initialiser statement

go
// Basic — no parentheses; braces required
score := 85
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")   // prints "B"
} else {
    fmt.Println("C")
}

// if with init statement — n and err scoped to this if block
if n, err := strconv.Atoi("42"); err == nil {
    fmt.Printf("parsed: %d\n", n)
} else {
    fmt.Printf("error: %v\n", err)
}
// n is NOT accessible here — this scoping prevents variable leaks

for — Go's only loop (replaces while and do-while)

go — three forms of for
// Form 1: C-style (init; condition; post)
for i := 0; i < 5; i++ {
    fmt.Print(i, " ")    // 0 1 2 3 4
}

// Form 2: while-style (condition only)
n := 1
for n < 1000 {
    n *= 2               // n becomes 1024
}

// Form 3: infinite — exit with break
for {
    if done() { break }
}

// range — iterate over arrays, slices, maps, strings, channels
fruits := []string{"apple", "banana", "cherry"}
for i, fruit := range fruits {
    fmt.Printf("[%d]=%s ", i, fruit)
}

// Discard index
for _, fruit := range fruits {
    fmt.Println(fruit)
}

// range over map — order is RANDOM every run
ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
    fmt.Printf("%s=%d ", name, age)
}

// range over string — (byte-index, rune) pairs
for i, r := range "Go🐹" {
    fmt.Printf("[%d]%c ", i, r)
}
// [0]G [1]o [2]🐹  ← index is byte offset, not character number

// break, continue, labeled break
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i+j >= 4 { break outer }   // exits BOTH loops
        fmt.Printf("(%d,%d) ", i, j)
    }
}
output (selected)odds<7: 1 3 5 c-style: 0 1 2 3 4 while power-of-2: 1 2 4 8 16 32 64 128 256 512 runes: [0]G [1]o [2]🐹

switch

go
// switch on a value — no fallthrough by default, no break needed
day := "Monday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("weekday")
case "Saturday", "Sunday":
    fmt.Println("weekend")
default:
    fmt.Println("unknown")
}

// switch with no condition — acts like an if-else chain
switch {
case score >= 90: fmt.Println("A")
case score >= 80: fmt.Println("B")
case score >= 70: fmt.Println("C")
default:          fmt.Println("F")
}

// switch with init statement
switch os := runtime.GOOS; os {
case "linux":  fmt.Println("Linux")
case "darwin": fmt.Println("macOS")
default:       fmt.Printf("Other: %s\n", os)
}

// fallthrough — explicitly opt into C-style cascade
switch 2 {
case 1: fmt.Println("one"); fallthrough
case 2: fmt.Println("two"); fallthrough     // executes
case 3: fmt.Println("three via fallthrough") // also executes
case 4: fmt.Println("four")                  // does NOT execute
}
switch fallthrough outputtwo three via fallthrough
§09

Arrays — Fixed-Size Value Types

An array's size is part of its type — [5]int and [10]int are different, incompatible types. Arrays are value types: assigning one copies all elements. In practice, slices (§10) are used far more often.

go
// Zero-valued array
var arr [5]int                   // [0 0 0 0 0]

// Array literal
primes := [6]int{2, 3, 5, 7, 11, 13}

// [...] lets the compiler count elements
fib := [...]int{1, 1, 2, 3, 5, 8, 13}
fmt.Println(len(fib))           // 7

// Access and modify
primes[0] = 999
fmt.Println(primes[0])         // 999

// 2-D array
var matrix [3][3]int
for i := range matrix {
    for j := range matrix[i] {
        matrix[i][j] = i*3 + j + 1
    }
}
// [[1 2 3] [4 5 6] [7 8 9]]

// Arrays are VALUE types — assignment copies all elements
a := [3]int{1, 2, 3}
b := a           // b is a complete, independent copy
b[0] = 999
fmt.Println(a)   // [1 2 3] — unchanged
fmt.Println(b)   // [999 2 3]

// Use pointer to avoid copying large arrays
func process(arr *[1000]int) { arr[0] = 42 }
outputprimes: [2 3 5 7 11 13 17 19] len=8 cap=8 original[0]=2 copy[0]=999 (arrays are value types) matrix: [[1 2 3] [4 5 6] [7 8 9]]
§10

Slices — Dynamic Arrays

A slice is a view into an underlying array with three components: a pointer, a length, and a capacity. Slices are Go's primary sequence type — arrays are rarely used directly.

go — creation, append, copy
// Slice literal
sl := []int{1, 2, 3, 4, 5}
fmt.Printf("len=%d cap=%d\n", len(sl), cap(sl))  // 5 5

// make([]T, length, capacity)
s2 := make([]int, 3, 8)   // len=3, cap=8 — pre-allocate room

// nil slice — no backing array
var ns []int
fmt.Println(ns == nil)    // true
fmt.Println(len(ns))      // 0 — safe to range over

// append — grows slice; allocates new backing array if capacity exceeded
sl = append(sl, 6, 7, 8)
spread := []int{9, 10}
sl = append(sl, spread...) // spread a slice into variadic with ...

// copy — independent copy with no shared backing array
dst := make([]int, 4)
n := copy(dst, sl)         // copies min(len(dst), len(sl)) elements

// Slice expressions [low : high]
//   low  defaults to 0
//   high defaults to len
sub := sl[1:4]    // elements 1, 2, 3 — shares backing array!
sub[0] = 999      // MODIFIES sl[1] too

// Three-index slice [low:high:max] — limits capacity of result
safe := sl[1:4:4] // cap of safe is 3, not 9 — prevents accidental append overlap
go — sort, stack ops, deleting
// sort.Slice with custom comparator
people := []struct{ Name string; Age int }{
    {"Charlie", 35}, {"Alice", 28}, {"Bob", 42}, {"Diana", 31},
}
sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age  // ascending by age
})
// [{Alice 28} {Diana 31} {Charlie 35} {Bob 42}]

// Stack (LIFO) operations using slice
var stack []int
// Push
stack = append(stack, 10, 20, 30)
// Pop
top  := stack[len(stack)-1]
stack = stack[:len(stack)-1]

// Delete element at index i (order not preserved — O(1))
i := 2
sl[i] = sl[len(sl)-1]   // overwrite with last
sl = sl[:len(sl)-1]      // shrink

// Delete element at index i (preserve order — O(n))
sl = append(sl[:i], sl[i+1:]...)

// Insert at index i
sl = append(sl[:i+1], sl[i:]...)  // make room
sl[i] = 99                          // place value
outputafter append: [1 2 3 4 5 6 7 8] len=8 cap=10 sl[2:5]=[3 4 5] sl[:3]=[1 2 3] sort by age: Charlie(35) Alice(28) Bob(42) Diana(31) → Alice(28) Diana(31) Charlie(35) Bob(42) stack push 10,20,30 → pop: top=30 remaining=[10 20]
Capacity growth: When append exceeds capacity, Go allocates a new backing array — roughly doubling capacity. The old and new slices no longer share memory. Always reassign the result: sl = append(sl, v).
§11

Maps

A map is an unordered hash table with O(1) average access. Key types must be comparable (support ==) — slices, maps, and functions cannot be map keys.

go
// make — always required before writing to a map
capitals := make(map[string]string)
capitals["France"]  = "Paris"
capitals["Germany"] = "Berlin"

// Map literal
ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Carol": 35,   // trailing comma required
}

// Read — missing key returns zero value silently
fmt.Println(ages["Alice"])  // 30
fmt.Println(ages["Dave"])   // 0  — NOT an error!

// Comma-ok idiom — distinguish zero value from missing key
if age, ok := ages["Alice"]; ok {
    fmt.Printf("Alice is %d\n", age)
}
if _, ok := ages["Dave"]; !ok {
    fmt.Println("Dave not found")
}

// delete — no error if key doesn't exist
delete(ages, "Bob")

// Iterate — ORDER IS RANDOM every run
for name, age := range ages {
    fmt.Printf("%s: %d\n", name, age)
}

// Sorted iteration — sort keys first
keys := make([]string, 0, len(ages))
for k := range ages { keys = append(keys, k) }
sort.Strings(keys)
for _, k := range keys { fmt.Printf("%s: %d\n", k, ages[k]) }

// Word frequency counter — classic map pattern
words := strings.Fields("go is great go is fast go")
freq  := make(map[string]int)
for _, w := range words { freq[w]++ }

// Map of slices — group-by pattern
type Person struct{ Name, Dept string }
people := []Person{{"Alice","Eng"},{"Bob","HR"},{"Carol","Eng"}}
byDept := make(map[string][]Person)
for _, p := range people { byDept[p.Dept] = append(byDept[p.Dept], p) }
output (word frequency)fast 1 go 3 great 1 is 2
Maps are not safe for concurrent use. Multiple goroutines reading is fine; any write requires exclusive access. Use sync.RWMutex around your map, or use sync.Map for read-heavy workloads.
III
Part III

Functions, Methods & Types

§12

Functions

go — function syntax
// Basic — func name(params) return-type
func add(x, y int) int {    // x and y share type
    return x + y
}

// Multiple return values — idiomatic Go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil   // nil signals no error
}

// Call it — always check the error
result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%.4f\n", result)   // 3.3333

// Named return values — self-documenting
func minMax(nums []int) (min, max int) {
    min, max = nums[0], nums[0]
    for _, n := range nums {
        if n < min { min = n }
        if n > max { max = n }
    }
    return   // naked return — returns min and max by name
}

// Variadic — accepts any number of ints
func sum(nums ...int) int {
    total := 0
    for _, n := range nums { total += n }
    return total
}
sum(1, 2, 3)                     // 6
sum([]int{1, 2, 3, 4, 5}...)    // spread a slice with ...

// Functions are first-class values
double  := func(n int) int { return n * 2 }
triple  := func(n int) int { return n * 3 }
apply   := func(f func(int) int, v int) int { return f(v) }
fmt.Println(apply(double, 5))    // 10
fmt.Println(apply(triple, 5))    // 15

// Named function type
type Transformer func(int) int
func compose(f, g Transformer) Transformer {
    return func(n int) int { return f(g(n)) }
}
doubleAndAdd1 := compose(func(n int) int { return n+1 }, double)
fmt.Println(doubleAndAdd1(4))    // (4*2)+1 = 9
output10/3=3.3333 minMax([5,1,9,3,7,4,8,2,6]) → min=1 max=9 sum(1..5)=15 sum(spread...)=100 apply(double,5)=10 apply(triple,5)=15
§13

Closures

A closure is a function that captures variables from its surrounding scope. The captured variables are shared between the closure and the code that created it — they live as long as the closure does.

go
// Counter factory — each call returns an independent counter
func makeCounter(start int) func() int {
    count := start   // captured by the returned closure
    return func() int {
        count++
        return count
    }
}
c1 := makeCounter(0)
c2 := makeCounter(100)
fmt.Println(c1(), c1(), c1())   // 1 2 3
fmt.Println(c2(), c2())         // 101 102

// Multiplier factory
func makeMultiplier(x float64) func(float64) float64 {
    return func(y float64) float64 { return x * y }
}
times3 := makeMultiplier(3)
fmt.Println(times3(4), times3(5))   // 12 15

// Memoisation — closure over a cache map
func memoize(f func(int) int) func(int) int {
    cache := make(map[int]int)
    return func(n int) int {
        if v, ok := cache[n]; ok { return v }  // cache hit
        result := f(n)
        cache[n] = result
        return result
    }
}
square := memoize(func(n int) int { return n * n })
fmt.Println(square(7), square(7))   // 49 49 (second call from cache)

// ── GOTCHA: loop variable capture ─────────────────────────────
// Wrong — all funcs capture the SAME variable i
funcs := make([]func() int, 3)
for i := 0; i < 3; i++ {
    funcs[i] = func() int { return i }  // i is shared!
}
// All print 3 (the final value after loop ends)

// Fix 1: shadow the variable with a new binding each iteration
for i := 0; i < 3; i++ {
    i := i   // new 'i' for each iteration
    funcs[i] = func() int { return i }
}
// Fix 2: pass as an argument
for i := 0; i < 3; i++ {
    funcs[i] = func(v int) func() int {
        return func() int { return v }
    }(i)
}
outputcounter: 1 2 3 4 5 ×3: 6.000000 15.000000 30.000000 memoize(7)=49 memoize(7)=49 actual calls=1 squares (fixed): [0 1 4]
§14

Pointers

A pointer stores a memory address. Go has pointers but no pointer arithmetic — you cannot increment a pointer. This removes entire classes of C-style memory bugs while keeping efficient data sharing.

go
x := 42
ptr := &x          // & = address-of; ptr has type *int
fmt.Println(x)     // 42
fmt.Println(ptr)   // 0xc000014088  (memory address)
fmt.Println(*ptr)  // 42  (* = dereference — read through pointer)

*ptr = 100         // write through pointer — modifies x
fmt.Println(x)     // 100

// new(T) — allocates a zero-value T, returns *T
p := new(int)      // *int pointing to a zero int
*p = 7
fmt.Println(*p)    // 7

// Pointer receiver — method modifies the receiver
type Counter struct{ n int }
func (c *Counter) Inc()    { c.n++ }
func (c  Counter) Value() int { return c.n }   // value receiver — read-only copy

c := Counter{}
c.Inc()   // Go auto-takes &c for the pointer receiver
c.Inc()
fmt.Println(c.Value())   // 2

// Function with pointer parameter — modifies caller's variable
func increment(n *int) { *n++ }
val := 10
increment(&val)
fmt.Println(val)   // 11

// Nil pointer — safe to test, PANICS to dereference
var np *int
fmt.Println(np == nil)   // true
// fmt.Println(*np)      // PANIC: nil pointer dereference
outputbefore Scale: (3.00, 4.00) after Scale(2): (6.00, 8.00) after *ptr=100: xp=100 counter after two Inc(): 2
§15

Structs & Embedding

go — struct basics
type Person struct {
    Name  string
    Age   int
    Email string
}

// Struct literal — named fields (preferred: explicit, order-independent)
alice := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}

// Struct literal — positional (fragile: breaks if fields are reordered)
bob := Person{"Bob", 25, "bob@example.com"}

// Partial — unset fields get zero values
carol := Person{Name: "Carol"}   // Age=0, Email=""

// Field access
fmt.Println(alice.Name)   // Alice
alice.Age++                // 31

// Pointer to struct — no arrow notation needed (Go auto-dereferences)
pp := &alice
pp.Age = 35               // same as (*pp).Age = 35

// Anonymous struct — for one-off data shapes, JSON/test fixtures
config := struct {
    Host string
    Port int
    TLS  bool
}{Host: "localhost", Port: 8080, TLS: true}

// Struct tags — used by encoding/json, database/sql, etc.
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"-"`             // omit from JSON
    Email    string `json:"email,omitempty"` // omit if empty
}
go — embedding (composition)
// Embedding promotes fields and methods from the embedded type
type Animal struct {
    Name  string
    Sound string
}
func (a Animal) Speak() string { return a.Name + " says " + a.Sound }

type Dog struct {
    Animal         // embedded — no field name, just the type
    Breed  string
}

rex := Dog{
    Animal: Animal{Name: "Rex", Sound: "Woof"},
    Breed:  "Labrador",
}

// Promoted fields — access directly
fmt.Println(rex.Name)         // Rex  (promoted from Animal)
fmt.Println(rex.Breed)        // Labrador

// Promoted method
fmt.Println(rex.Animal.Speak())  // Rex says Woof  (explicit Animal's)
fmt.Println(rex.Speak())         // Rex says Woof  (promoted)

// Dog can override the promoted method
func (d Dog) Speak() string { return d.Name + " barks!" }
fmt.Println(rex.Speak())         // Rex barks!     (Dog's override)
fmt.Println(rex.Animal.Speak())  // Rex says Woof  (still accessible)
outputAlice (age 31) Rex at Acme age=26 — promoted Name and Birthday() method work Dog.Speak(): Rex barks! Animal.Speak(): Rex says Woof
§16

Methods

A method is a function with a receiver — a named type that the function is bound to. Receivers appear between func and the method name.

go
type Rectangle struct {
    Width, Height float64
}

// Value receiver — gets a COPY of the struct; cannot modify original
func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) String() string {
    return fmt.Sprintf("Rect(%.1f×%.1f)", r.Width, r.Height)
}

// Pointer receiver — can modify the struct
func (r *Rectangle) Scale(factor float64) {
    r.Width  *= factor
    r.Height *= factor
}
func (r *Rectangle) SetWidth(w float64) {
    if w > 0 { r.Width = w }
}

rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area())       // 50
fmt.Println(rect.Perimeter())  // 30
fmt.Println(rect)              // Rect(10.0×5.0)  ← fmt calls String()

rect.Scale(2)                  // Go auto-takes &rect for pointer receiver
fmt.Println(rect.Area())       // 200

// Methods on non-struct types — you can add methods to ANY named type
type Celsius float64
type Fahrenheit float64

func (c Celsius)    ToF() Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func (f Fahrenheit) ToC() Celsius    { return Celsius((f-32) * 5/9) }

boiling := Celsius(100)
fmt.Printf("%.1f°C = %.1f°F\n", boiling, boiling.ToF()) // 100.0°C = 212.0°F

// Method value — bind a method to a specific receiver
scaleFn := rect.Scale     // scaleFn has type func(float64)
scaleFn(0.5)              // equivalent to rect.Scale(0.5)

Value vs Pointer receiver — decision table

Use pointer receiver when…Use value receiver when…
Method needs to modify the receiverMethod is read-only and struct is small
Struct is large (copying is expensive)Type should behave like a built-in (int, string)
Any other method on the type uses a pointer receiverYou want a snapshot of the value at the time of call
Consistency rule: If any method on a type uses a pointer receiver, all methods on that type should use pointer receivers. Mixed receiver types cause confusion about whether you need a pointer to call a method.
§17

Interfaces

An interface defines a set of method signatures. A type implicitly satisfies an interface by implementing all its methods — no implements keyword, no explicit declaration needed. This is compile-time duck typing.

go — defining and satisfying
// Interface declaration — just a list of method signatures
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Any type with these methods satisfies Shape — automatically
type Circle struct{ Radius float64 }
func (c Circle) Area()      float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }

type Rect struct{ W, H float64 }
func (r Rect) Area()      float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }

// Interface variable holds (concrete type, value) pair
var s Shape = Circle{Radius: 5}
fmt.Printf("Area: %.2f\n", s.Area())     // 78.54

s = Rect{W: 10, H: 4}   // reassign a different concrete type
fmt.Printf("Area: %.2f\n", s.Area())     // 40.00

// Polymorphism — function works with any Shape
func totalArea(shapes []Shape) float64 {
    total := 0.0
    for _, sh := range shapes {
        total += sh.Area()
    }
    return total
}
total := totalArea([]Shape{Circle{5}, Rect{10, 4}})  // 118.54
go — type assertion, type switch
// Type assertion — extract the concrete value
var i interface{} = Circle{Radius: 3}

// Safe form — never panics
c, ok := i.(Circle)
if ok {
    fmt.Printf("Circle radius: %.0f\n", c.Radius)  // 3
}

// Unsafe form — panics if wrong type
c2 := i.(Circle)   // panics if i is not a Circle

// Type switch — identify the concrete type
func describe(v interface{}) {
    switch tv := v.(type) {
    case int:
        fmt.Printf("int: %d\n", tv)
    case string:
        fmt.Printf("string: %q len=%d\n", tv, len(tv))
    case Shape:
        fmt.Printf("Shape area=%.2f\n", tv.Area())
    case nil:
        fmt.Println("nil")
    default:
        fmt.Printf("unknown type: %T\n", tv)
    }
}

// Key stdlib interfaces
type Stringer interface{ String() string }          // fmt.Stringer
type error    interface{ Error() string }           // built-in
type Reader   interface{ Read([]byte) (int, error) } // io.Reader
type Writer   interface{ Write([]byte) (int, error) } // io.Writer

// io.Reader / io.Writer — Go's most important interfaces
// A function accepting io.Reader works with files, strings, network, zip, etc.
func processData(r io.Reader) { ... }  // reads from ANYTHING
outputmain.Circle area= 78.5398 perim= 31.4159 main.Rect area= 40.0000 perim= 28.0000 main.Triangle area= 6.0000 perim= 12.0000 total area = 124.5398 asserted Circle: r=3 area=28.2743
IV
Part IV

Errors, Concurrency & Synchronisation

§18

Error Handling

Go has no exceptions. Errors are ordinary values returned as the last return value. Calling code must explicitly check them — there is no hidden control flow from try/catch.

go — the (value, error) pattern
// The error interface — just one method
type error interface {
    Error() string
}

// errors.New — for simple, static error values
var ErrNotFound  = errors.New("not found")
var ErrDivByZero = errors.New("division by zero")

// fmt.Errorf with %w — adds context and wraps the original
func readConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("readConfig %q: %w", path, err)
    }
    //...
}

// ── Custom error type ─────────────────────────────────────────
type ParseError struct {
    Input string
    Err   error
}
func (e *ParseError) Error() string {
    return fmt.Sprintf("cannot parse %q: %v", e.Input, e.Err)
}
func (e *ParseError) Unwrap() error { return e.Err }  // enables errors.Is/As

// ── errors.Is — test for a sentinel error in the chain ────────
_, err := findUser(-1)
if errors.Is(err, ErrNotFound) {
    fmt.Println("create a new user?")
}

// ── errors.As — extract a specific type from the chain ────────
_, err = parse("not-a-number")
var pe *ParseError
if errors.As(err, &pe) {
    fmt.Printf("bad input: %q\n", pe.Input)
}

// ── Don't ignore errors ────────────────────────────────────────
// Bad:
result, _ := riskyOperation()   // ignoring the error

// Good:
result, err := riskyOperation()
if err != nil {
    return fmt.Errorf("myFunc: %w", err)  // wrap with context and propagate
}
outputsafeParse("42") = 42 ErrNegative: "-1" ParseError: input="abc" safeParse("100") = 100 errors.Is(wrapped, base) = true

Error handling pattern reference

PatternWhen to use
return errPropagate as-is — caller has enough context
fmt.Errorf("ctx: %w", err)Add context (function name, key value) while preserving type for errors.Is/As
errors.Is(err, sentinel)Test if the error chain contains a specific sentinel error
errors.As(err, &target)Extract a specific concrete error type from the chain
log.Fatalf(err)Unrecoverable in main() — log and exit
panic(err)Programmer error only (nil pointer, out of bounds) — not for user errors
§19

defer · panic · recover

go — defer
// defer — runs when the surrounding function returns (any path)
// Deferred calls run LIFO (last-in, first-out)
// Arguments are evaluated IMMEDIATELY at the defer statement

func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil { return err }
    defer f.Close()      // guaranteed to run, even on early return or panic

    mu.Lock()
    defer mu.Unlock()    // guaranteed — never forget to unlock

    // ...do work...
    return nil
}

// Multiple defers — LIFO demonstration
func demo() {
    defer fmt.Println("first defer — runs LAST")
    defer fmt.Println("second defer")
    defer fmt.Println("third defer — runs FIRST")
    fmt.Println("function body")
}
// Output:
// function body
// third defer — runs FIRST
// second defer
// first defer — runs LAST
go — panic and recover
// panic — stops normal execution; deferred functions still run
// Use only for programmer errors (index out of range, nil pointer)
// Do NOT use as a substitute for error returns

func mustPositive(n int) int {
    if n <= 0 { panic(fmt.Sprintf("mustPositive: got %d", n)) }
    return n
}

// recover — catches a panic inside a deferred function ONLY
func safeDiv(a, b float64) (result float64, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    if b == 0 { panic("cannot divide by zero") }
    return a / b, nil
}

result, err := safeDiv(10, 0)
// result=0  err="recovered from panic: cannot divide by zero"

result, err = safeDiv(10, 4)
// result=2.5  err=nil

// Common pattern: HTTP servers use this to catch panics
// in handler goroutines and return 500 instead of crashing
outputfunction body deferred C (runs 1st) deferred B (runs 2nd) deferred A (runs 3rd) safeDiv(10,0): result=0 err=recovered from panic: cannot divide by zero safeDiv(10,4): result=2.50
§20

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. Starting one costs about 2 KB of initial stack (an OS thread costs ~1 MB). You can run hundreds of thousands concurrently. The go keyword is all it takes.

go
// go keyword — non-blocking; returns immediately
go fmt.Println("running in background")

// Anonymous goroutine
go func() {
    fmt.Println("also in background")
}()

// Pass data as arguments — don't capture loop variables
for i := 0; i < 5; i++ {
    go func(id int) {
        fmt.Printf("worker %d\n", id)
    }(i)   // i is passed by value — safe
}

// ── sync.WaitGroup — wait for goroutines to finish ─────────────
var wg sync.WaitGroup

results := make([]int, 6)
for i := 0; i < 6; i++ {
    wg.Add(1)                 // increment BEFORE launching
    go func(id int) {
        defer wg.Done()       // decrement when done
        results[id] = id * id
    }(i)
}
wg.Wait()   // blocks until counter reaches zero
fmt.Println("results:", results)

// ── sync/atomic — lock-free integer operations ─────────────────
import "sync/atomic"

var counter int64
for i := 0; i < 1000; i++ {
    go func() { atomic.AddInt64(&counter, 1) }()
}
// After all goroutines finish: counter == 1000

// ── GOMAXPROCS — parallel goroutines ──────────────────────────
runtime.GOMAXPROCS(4)   // use 4 OS threads (default: CPU count)
outputgoroutine squares: [0 1 4 9 16 25] atomic counter after 1000 goroutines: 1000
Goroutine leaks: A goroutine blocked on a channel receive with no sender will run forever. Always design goroutines with a clear exit path — use a done channel or context.Context for cancellation.
§21

Channels & select

Channels are typed conduits for safe goroutine communication. Go's concurrency mantra: "Do not communicate by sharing memory; share memory by communicating."

go — channel basics
// Unbuffered — both sender and receiver block until both are ready
ch := make(chan int)
go func() { ch <- 42 }()
v := <-ch              // synchronises with the goroutine
fmt.Println(v)         // 42

// Buffered — send doesn't block until buffer is full
buf := make(chan string, 3)
buf <- "one"; buf <- "two"; buf <- "three"   // all non-blocking
fmt.Println(<-buf)   // "one"

// Directional channel types — documented in function signatures
func producer(ch chan<- int) {   // send-only
    for i := 0; i < 5; i++ { ch <- i }
    close(ch)   // MUST close to end a range loop
}
func consumer(ch <-chan int) {   // receive-only
    for v := range ch { fmt.Println(v) }
}

// Range over a channel — loops until it's closed
numbers := make(chan int)
go func() {
    for i := 1; i <= 5; i++ { numbers <- i * i }
    close(numbers)   // signals range to stop
}()
for sq := range numbers { fmt.Println(sq) }   // 1 4 9 16 25

// Pipeline pattern — chain of goroutines connected by channels
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for _, n := range nums { out <- n } }()
    return out
}
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for n := range in { out <- n * n } }()
    return out
}
for v := range square(generate(2, 3, 4, 5)) {
    fmt.Print(v, " ")   // 4 9 16 25
}
go — select, timeout, done channel
// select — multiplex multiple channel operations (like switch for channels)
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "one"
select {
case msg := <-ch1: fmt.Println("ch1:", msg)
case msg := <-ch2: fmt.Println("ch2:", msg)
}

// select with default — non-blocking channel operation
select {
case v := <-ch: fmt.Println("got:", v)
default:        fmt.Println("no value ready")
}

// Timeout pattern
select {
case result := <-slowOperation():
    fmt.Println("got:", result)
case <-time.After(2 * time.Second):
    fmt.Println("timed out")
}

// Done channel — cancellation broadcast
done := make(chan struct{})   // struct{} costs zero bytes
numbers := make(chan int)
go func() {
    defer close(numbers)
    for i := 0; ; i++ {
        select {
        case <-done:   return   // exit goroutine cleanly
        case numbers <- i:
        }
    }
}()
// Consume 5 numbers then cancel
count := 0
for n := range numbers {
    fmt.Print(n, " "); count++
    if count >= 5 { close(done); break }
}
// 0 1 2 3 4
outputpipeline squares: 1 4 9 16 25 36 49 64 buffered: one two three four select timeout: received "done" in 1ms done-channel: 0 1 2 3 4
§22

sync Package

go
// ── sync.Mutex — exclusive lock ────────────────────────────────
type SafeCounter struct {
    mu sync.Mutex
    n  int
}
func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()   // always defer Unlock
    c.n++
}
func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n
}

// ── sync.RWMutex — multiple concurrent readers, exclusive writer ─
type Cache struct {
    mu    sync.RWMutex
    items map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()           // many goroutines can hold RLock simultaneously
    defer c.mu.RUnlock()
    v, ok := c.items[key]
    return v, ok
}
func (c *Cache) Set(key, val string) {
    c.mu.Lock()            // exclusive — blocks all readers and writers
    defer c.mu.Unlock()
    c.items[key] = val
}

// ── sync.Once — run exactly once regardless of goroutines ───────
var (
    db   *Database
    once sync.Once
)
func GetDB() *Database {
    once.Do(func() {
        db = connect()   // called exactly once, even from 1000 goroutines
    })
    return db
}

// ── sync/atomic — lock-free operations ──────────────────────────
import "sync/atomic"
var counter int64
atomic.AddInt64(&counter, 1)       // thread-safe increment
atomic.StoreInt64(&counter, 0)     // thread-safe store
val := atomic.LoadInt64(&counter)  // thread-safe load
atomic.CompareAndSwapInt64(&counter, 0, 1)  // CAS
outputmutex counter = 100 sync.Once: initialised exactly once!
V
Part V

Ecosystem, Generics & Best Practices

§23

Standard Library Highlights

go — strings package
import "strings"

s := "  The Quick Brown Fox  "
strings.TrimSpace(s)                   // "The Quick Brown Fox"
strings.ToUpper(s)                     // "  THE QUICK BROWN FOX  "
strings.ToLower(s)                     // "  the quick brown fox  "
strings.Contains(s, "Fox")            // true
strings.HasPrefix(strings.TrimSpace(s), "The")  // true
strings.HasSuffix(strings.TrimSpace(s), "Fox")  // true
strings.Count("cheese", "e")          // 3
strings.Index("Hello", "ll")          // 2
strings.Replace("aaa", "a", "b", 2)  // "bba"  (replace first 2)
strings.ReplaceAll("aaa", "a", "b")   // "bbb"
strings.Split("a,b,c", ",")          // ["a" "b" "c"]
strings.Fields("  foo  bar  ")        // ["foo" "bar"]  (splits on whitespace)
strings.Join([]string{"a","b","c"}, "-")  // "a-b-c"
strings.Repeat("go!", 3)              // "go!go!go!"
strings.TrimPrefix("Hello", "He")     // "llo"
strings.TrimSuffix("Hello", "lo")     // "Hel"
strings.Cut("key=value", "=")         // "key", "value", true

// strings.Builder — O(n) concatenation
var b strings.Builder
for i := 0; i < 5; i++ { fmt.Fprintf(&b, "item%d ", i) }
fmt.Println(strings.TrimSpace(b.String()))  // item0 item1 item2 item3 item4
go — sort, math, time
import ("sort"; "math"; "time")

// sort
ints := []int{3,1,4,1,5,9,2,6}; sort.Ints(ints)    // [1 1 2 3 4 5 6 9]
strs := []string{"banana","apple","cherry"}; sort.Strings(strs)
sort.Slice(items, func(i,j int) bool { return items[i].Key < items[j].Key })
idx := sort.SearchInts(ints, 5)   // binary search — index where 5 is/should be

// math — common functions
math.Sqrt(144)       // 12
math.Pow(2, 32)      // 4294967296
math.Abs(-3.14)      // 3.14
math.Floor(3.7)      // 3  (truncate toward -∞)
math.Ceil(3.2)       // 4  (round toward +∞)
math.Round(3.5)      // 4
math.Log(math.E)     // 1  (natural log)
math.Log2(1024)      // 10
math.Log10(1000)     // 3
math.Pi              // 3.141592653589793
math.MaxInt64        // 9223372036854775807
math.MaxFloat64      // 1.7976931348623157e+308

// time — Go's reference time is: Mon Jan 2 15:04:05 MST 2006
now := time.Now()
now.Format("2006-01-02")          // "2025-06-02"
now.Format("15:04:05")            // "14:30:00"
now.Format(time.RFC3339)          // "2025-06-02T14:30:00Z"
now.Add(24 * time.Hour)           // tomorrow
now.AddDate(0, 1, 0)              // next month
tomorrow.Sub(now)                 // 24h0m0s (time.Duration)
t, _ := time.Parse("2006-01-02", "2025-01-01")
time.Since(start)                 // elapsed since start
time.Until(deadline)              // remaining until deadline
outputTrimSpace: "the quick brown fox" Fields: [the quick brown fox] sort.Ints: [1 1 2 3 4 5 6 9] sort.Strings: [apple banana cherry date] time.Now: 2025-06-02 14:30:00
§24

Generics (Go 1.18+)

Generics let you write functions and types that work with multiple types while keeping full type safety. They use type parameters in square brackets.

go — type constraints and generic functions
// Type constraint — a union of types
type Number interface {
    int | int8 | int16 | int32 | int64 | float32 | float64
}

// ~T means: T AND any type whose underlying type is T
type Ordered interface {
    ~int | ~float64 | ~string
}

// Generic function — [T Ordered] is the type parameter list
func Min[T Ordered](a, b T) T { if a < b { return a }; return b }
func Max[T Ordered](a, b T) T { if a > b { return a }; return b }

// Type is inferred from arguments — no need to specify
Min(3, 7)              // int: 3
Min(3.14, 2.72)        // float64: 2.72
Min("apple", "banana") // string: "apple"

// Multiple type parameters
func Map[T, U any](s []T, f func(T) U) []U {
    r := make([]U, len(s))
    for i, v := range s { r[i] = f(v) }
    return r
}

func Filter[T any](s []T, pred func(T) bool) []T {
    var r []T
    for _, v := range s {
        if pred(v) { r = append(r, v) }
    }
    return r
}

func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
    acc := init
    for _, v := range s { acc = f(acc, v) }
    return acc
}

// Examples
Map([]int{1,2,3,4,5}, func(n int) int { return n*2 })
// [2 4 6 8 10]

Map([]int{1,2,3}, strconv.Itoa)
// ["1" "2" "3"]

Filter([]int{1,2,3,4,5,6}, func(n int) bool { return n%2 == 0 })
// [2 4 6]

Reduce([]int{1,2,3,4,5}, 0, func(acc, v int) int { return acc+v })
// 15 (sum)
go — generic Stack[T]
// Generic type — works with any type T
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(v T) {
    s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T         // zero value of T
        return zero, false
    }
    top := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return top, true
}

func (s *Stack[T]) Peek() (T, bool) {
    if len(s.items) == 0 { var z T; return z, false }
    return s.items[len(s.items)-1], true
}

func (s Stack[T]) Len() int { return len(s.items) }

// Use with any type — fully type-safe
var ints Stack[int]
ints.Push(1); ints.Push(2); ints.Push(3)
v, ok := ints.Pop()    // v is int(3), ok=true

var strs Stack[string]
strs.Push("Go"); strs.Push("is"); strs.Push("awesome")
w, _ := strs.Pop()     // w is string("awesome")
outputMin(3,7)=3 Min("apple","banana")="apple" Max(3.14,2.72)=3.14 SumSlice(ints)=15 SumSlice(floats)=6.6 Map(×2): [2 4 6 8 10] Map(Itoa): [1 2 3] Filter(even): [2 4 6 8] Reduce(+): 15 stack pop: "awesome" "is" "Go"
§25

Testing with go test

Go has a built-in testing framework. Test files end in _test.go and are excluded from normal builds.

gomath_test.go
package math   // or: package math_test for black-box tests

import (
    "testing"
    "errors"
)

// Test function — must start with Test, take *testing.T
func TestAdd(t *testing.T) {
    got  := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d, want %d", got, want)
    }
}

// Table-driven test — idiomatic Go
func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"positive",     10, 2,  5.0, false},
        {"zero divisor",  5, 0,  0.0, true},
        {"negative",     -6, 2, -3.0, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {   // subtests — run individually
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !tt.wantErr && got != tt.want {
                t.Errorf("Divide() = %v, want %v", got, tt.want)
            }
        })
    }
}

// Benchmark — name must start with Benchmark
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {   // b.N is set by the framework
        Add(10, 20)
    }
}

// Example — verified by go test; shown in go doc
func ExampleAdd() {
    fmt.Println(Add(2, 3))
    // Output:
    // 5
}
bash — running tests
go test ./...                      # run all tests in all packages
go test -v ./...                   # verbose output (show each test)
go test -run TestDivide            # run tests matching regex
go test -run TestDivide/zero       # run a specific subtest
go test -bench=.                   # run all benchmarks
go test -bench=BenchmarkAdd -benchmem  # with memory stats
go test -cover                     # show test coverage %
go test -coverprofile=c.out && go tool cover -html=c.out  # HTML coverage report
go test -race ./...                # run with race detector (ALWAYS in CI)
go test -count=1 ./...             # disable test cache
§26

Packages & Modules

bash — module commands
go mod init github.com/yourname/myapp  # create go.mod
go get github.com/pkg/name@v1.2.3      # add dependency
go get -u ./...                         # upgrade all dependencies
go mod tidy                             # remove unused, add missing
go mod vendor                           # copy deps to vendor/
go mod graph                            # show dependency graph
go — package layout
// myapp/
// ├── go.mod
// ├── go.sum         (lock file — commit this)
// ├── main.go
// └── internal/      (only importable by this module)
//     └── mathutil/
//         ├── math.go
//         └── math_test.go

// mathutil/math.go
package mathutil   // package name == directory name (convention)

// Exported — starts with uppercase — visible outside the package
func Add(a, b int) int { return a + b }

// Unexported — starts with lowercase — package-private
func helper(n int) int { return n * 2 }

// ─────────────────────────────────────────────────────────────
// main.go
package main

import (
    "fmt"
    "github.com/yourname/myapp/internal/mathutil"

    // Import aliases
    mth "github.com/yourname/myapp/internal/mathutil"

    // Blank import — runs init() only (side effects)
    _ "github.com/lib/pq"
)

func main() {
    fmt.Println(mathutil.Add(3, 4))   // 7
    fmt.Println(mth.Add(3, 4))        // 7 via alias
    // mathutil.helper(5)  // ERROR: unexported
}

// ── init() ────────────────────────────────────────────────────
// init() runs before main(), after var declarations
// Multiple init() functions allowed per package
func init() {
    // One-time setup — register drivers, validate config, etc.
    // Avoid complex logic here — prefer explicit initialisation
}

Visibility rules — capitalisation only

Starts withVisibilityExamples
UppercaseExported — accessible from any packagePrintln, Error, Config, UserID
LowercaseUnexported — this package onlyparse, internalState, helper
§27

Best Practices & Idioms

Go Proverbs (Rob Pike)

  • "Don't communicate by sharing memory; share memory by communicating."
  • "Errors are values."
  • "Don't just check errors, handle them gracefully."
  • "The bigger the interface, the weaker the abstraction."
  • "Make the zero value useful."
  • "A little copying is better than a little dependency."
  • "Clear is better than clever."
  • "Accept interfaces, return structs."

Naming conventions

ThingStyleExamples
Variables, functionscamelCasenumItems, parseConfig, isValid
Exported symbolsPascalCaseReadFile, UserID, HTTPClient
AcronymsAll capsuserID not userId; parseURL not parseUrl
Single-method interfaces-er suffixReader, Stringer, Handler, Logger
Packageslowercase, shorthttp, fmt, strconv, mathutil
Sentinel errorsErrXxxErrNotFound, ErrTimeout, io.EOF

Code quality checklist

  • Run gofmt (or configure editor to format on save)
  • Run go vet ./... before every commit
  • Run go test -race ./... in CI
  • Always check error returns — never discard with _ in production
  • Wrap errors with context: fmt.Errorf("functionName: %w", err)
  • Accept interfaces, return concrete types
  • Keep interfaces small — prefer single-method interfaces
  • Use defer for cleanup (Close, Unlock, Done)
  • Document all exported symbols with a comment starting with the name
  • Use make(map[K]V) before writing to a map
  • Don't use panic in library code — return errors
  • Don't start goroutines without a clear exit path
  • Don't use interface{} when generics work (Go 1.18+)
  • Don't use init() for complex setup — prefer explicit calls
  • Don't mix value and pointer receivers on the same type
go — idiomatic patterns at a glance
// Error handling
if err != nil {
    return fmt.Errorf("context: %w", err)
}

// Comma-ok idiom
v, ok := m[key]          // map: ok=false if key absent
v, ok := i.(T)           // type assertion: ok=false if wrong type
v, ok := <-ch            // channel: ok=false if channel closed

// Defer cleanup
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()

// Accept interfaces
func process(r io.Reader) { ... }   // not: func process(f *os.File)

// Table-driven tests
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) { ... })
}

// Init a struct to its zero value (make the zero value useful)
var mu sync.Mutex   // ready to Lock() immediately — no New() needed
var b bytes.Buffer  // ready to Write() immediately

Complete Tutorial Program

This single file covers all 25 sections above. Run with go run go_tutorial.go and compare the output to each section. Every concept is verified — this file compiles and runs cleanly on Go 1.22.

bash
# Download and run
go run go_tutorial.go

# Or build a permanent binary
go build -o go_tutorial go_tutorial.go && ./go_tutorial
gogo_tutorial.go
// =============================================================================
//  go_tutorial.go  —  Complete Go Language Tutorial
//  Run:   go run go_tutorial.go
//  Build: go build -o go_tutorial go_tutorial.go && ./go_tutorial
//  Covers §03–§25 from the guide. Each section prints its output.
// =============================================================================
package main

import (
	"errors"
	"fmt"
	"math"
	"math/cmplx"
	"os"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"unicode/utf8"
)

// ─── §07 Constants & iota ─────────────────────────────────────────────────────
const (
	AppName     = "GoTutorial"
	GoldenRatio = 1.618033988749895
)

type Direction int

const (
	North Direction = iota; East; South; West
)

func (d Direction) String() string {
	return [...]string{"North", "East", "South", "West"}[d]
}

type ByteSize float64

const (
	_           = iota
	KB ByteSize = 1 << (10 * iota)
	MB; GB; TB
)

type Permission uint

const (
	Read    Permission = 1 << iota
	Write
	Execute
)

func (p Permission) String() string {
	var parts []string
	if p&Read != 0    { parts = append(parts, "r") } else { parts = append(parts, "-") }
	if p&Write != 0   { parts = append(parts, "w") } else { parts = append(parts, "-") }
	if p&Execute != 0 { parts = append(parts, "x") } else { parts = append(parts, "-") }
	return strings.Join(parts, "")
}

// ─── §09 Structs & Methods ────────────────────────────────────────────────────
type Point struct{ X, Y float64 }

func (p Point) Distance(o Point) float64 {
	return math.Sqrt((p.X-o.X)*(p.X-o.X) + (p.Y-o.Y)*(p.Y-o.Y))
}
func (p *Point) Scale(f float64) { p.X *= f; p.Y *= f }
func (p Point) String() string   { return fmt.Sprintf("(%.2f, %.2f)", p.X, p.Y) }

type Animal struct{ Name, Sound string }
func (a Animal) Speak() string  { return a.Name + " says " + a.Sound }

type Dog struct {
	Animal
	Breed string
}
func (d Dog) Speak() string { return d.Name + " barks!" }

type Person struct{ Name string; Age int; Email string }
func (p Person) String() string { return fmt.Sprintf("%s (age %d)", p.Name, p.Age) }
func (p *Person) Birthday()     { p.Age++ }

// ─── §10 Interfaces ────────────────────────────────────────────────────────────
type Shape interface{ Area() float64; Perimeter() float64 }

type Circle   struct{ Center Point; Radius float64 }
type Rect     struct{ W, H float64 }
type Triangle struct{ A, B, C float64 }

func (c Circle)   Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle)   Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c Circle)   String() string     { return fmt.Sprintf("Circle(r=%.2f)", c.Radius) }
func (r Rect)     Area() float64      { return r.W * r.H }
func (r Rect)     Perimeter() float64 { return 2 * (r.W + r.H) }
func (r Rect)     String() string     { return fmt.Sprintf("Rect(%.0fx%.0f)", r.W, r.H) }
func (t Triangle) Area() float64 {
	s := (t.A + t.B + t.C) / 2
	return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }

// ─── §11 Errors ─────────────────────────────────────────────────────────────────
var ErrDivByZero = errors.New("division by zero")
var ErrNegative  = errors.New("negative value")

type ParseError struct{ Input string; Err error }
func (e *ParseError) Error() string { return fmt.Sprintf("cannot parse %q: %v", e.Input, e.Err) }
func (e *ParseError) Unwrap() error { return e.Err }

func safeParse(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil { return 0, &ParseError{Input: s, Err: err} }
	if n < 0      { return 0, fmt.Errorf("safeParse(%q): %w", s, ErrNegative) }
	return n, nil
}

func divide(a, b float64) (float64, error) {
	if b == 0 { return 0, ErrDivByZero }
	return a / b, nil
}

func minMax(ns []int) (min, max int) {
	min, max = ns[0], ns[0]
	for _, n := range ns {
		if n < min { min = n }
		if n > max { max = n }
	}
	return
}

// ─── §12 Closures ─────────────────────────────────────────────────────────────
func makeCounter(start int) func() int {
	c := start
	return func() int { c++; return c }
}
func makeMultiplier(x float64) func(float64) float64 {
	return func(y float64) float64 { return x * y }
}

// ─── §13 defer/panic/recover ──────────────────────────────────────────────────
func safeDiv(a, b float64) (result float64, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()
	if b == 0 { panic("cannot divide by zero") }
	return a / b, nil
}

// ─── §14 Goroutines & Channels ────────────────────────────────────────────────
func genNums(nums ...int) <-chan int {
	out := make(chan int)
	go func() { defer close(out); for _, n := range nums { out <- n } }()
	return out
}
func squarePipe(in <-chan int) <-chan int {
	out := make(chan int)
	go func() { defer close(out); for n := range in { out <- n * n } }()
	return out
}

// ─── §15 Generics ─────────────────────────────────────────────────────────────
type Ordered interface{ ~int | ~float64 | ~string }

func Min[T Ordered](a, b T) T { if a < b { return a }; return b }
func Max[T Ordered](a, b T) T { if a > b { return a }; return b }

func SumSlice[T interface{ ~int | ~float64 }](s []T) T {
	var t T; for _, v := range s { t += v }; return t
}
func MapSlice[T, U any](s []T, f func(T) U) []U {
	r := make([]U, len(s)); for i, v := range s { r[i] = f(v) }; return r
}
func FilterSlice[T any](s []T, p func(T) bool) []T {
	var r []T; for _, v := range s { if p(v) { r = append(r, v) } }; return r
}
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
	acc := init; for _, v := range s { acc = f(acc, v) }; return acc
}

type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
	if len(s.items) == 0 { var z T; return z, false }
	top := s.items[len(s.items)-1]; s.items = s.items[:len(s.items)-1]; return top, true
}
func (s *Stack[T]) Peek() (T, bool) {
	if len(s.items) == 0 { var z T; return z, false }
	return s.items[len(s.items)-1], true
}
func (s Stack[T]) Len() int { return len(s.items) }

// ─── Helpers ──────────────────────────────────────────────────────────────────
func section(n int, title string) {
	fmt.Printf("\n%s\n  §%02d  %s\n%s\n",
		strings.Repeat("=", 60), n, title, strings.Repeat("=", 60))
}
func sub(t string) { fmt.Printf("\n  -- %s --\n", t) }

// ─── main ─────────────────────────────────────────────────────────────────────
func main() {
	fmt.Printf("  Go Tutorial  (binary: %s)\n", os.Args[0])

	section(3, "Hello, Go!")
	fmt.Printf("  App=%q  GoldenRatio=%.6f\n", AppName, GoldenRatio)
	fmt.Printf("  fmt verbs: %%d=%d  %%.2f=%.2f  %%q=%q  %%t=%t  %%T=%T\n",
		42, 3.14159, "go", true, Circle{})

	section(4, "Variables & Zero Values")
	var i int; var f float64; var s string; var b bool; var p *int
	fmt.Printf("  zeros: int=%d  float64=%g  string=%q  bool=%t  *int=%v\n", i, f, s, b, p)
	x := 42; y := 3.14; name := "Gopher"
	fmt.Printf("  short: x=%d  y=%.2f  name=%q\n", x, y, name)
	a, b2 := 100, 200; a, b2 = b2, a
	fmt.Printf("  swap:  a=%d  b=%d\n", a, b2)

	section(5, "Types")
	sub("Integers")
	var i8 int8 = 127; var u32 uint32 = 4_294_967_295; var i64 int64 = -9_223_372_036_854_775_808
	fmt.Printf("  int8=%d  uint32=%d  int64=%d\n", i8, u32, i64)
	sub("Float & complex")
	var f32 float32 = 3.14; var f64 float64 = math.Pi
	var c128 complex128 = 3 + 4i
	fmt.Printf("  float32=%g  float64=%.10f\n", f32, f64)
	fmt.Printf("  complex128=%v  abs=%.2f  arg=%.4f\n",
		c128, cmplx.Abs(c128), cmplx.Phase(c128))
	sub("Strings and runes")
	str := "Hello, 世界 !"
	fmt.Printf("  %q: bytes=%d  runes=%d\n", str, len(str), utf8.RuneCountInString(str))
	fmt.Printf("  byte[0]=%d (%c)  rune[7]=%d (%c)\n",
		str[0], str[0], []rune(str)[7], []rune(str)[7])

	section(6, "Type Conversions & strconv")
	intVal := 42
	floatVal := float64(intVal); uintVal := uint(floatVal * 1.5)
	fmt.Printf("  int->float64: %v  float64*1.5->uint: %v\n", floatVal, uintVal)
	fmt.Printf("  Itoa(255)=%q  ParseFloat=%g\n",
		strconv.Itoa(255),
		func() float64 { v, _ := strconv.ParseFloat("3.14159", 64); return v }())
	fmt.Printf("  FormatInt(255,16)=%q  FormatInt(255,2)=%q\n",
		strconv.FormatInt(255, 16), strconv.FormatInt(255, 2))

	section(7, "Constants & iota")
	for _, d := range []Direction{North, East, South, West} {
		fmt.Printf("  %d=%s ", d, d)
	}
	fmt.Println()
	fmt.Printf("  KB=%.0f  MB=%.0f  GB=%.0f  TB=%.0f\n",
		float64(KB), float64(MB), float64(GB), float64(TB))
	for _, p2 := range []Permission{0, Read, Write, Read | Write, Read | Write | Execute} {
		fmt.Printf("  Permission(%d)=%s\n", p2, p2)
	}

	section(8, "Control Flow")
	sub("if with init")
	if n2, err := strconv.Atoi("42"); err == nil { fmt.Printf("  parsed: %d\n", n2) }
	sub("for — three forms")
	fmt.Print("  c-style: "); for i := 0; i < 5; i++ { fmt.Printf("%d ", i) }; fmt.Println()
	fmt.Print("  while:   "); pw := 1; for pw < 256 { fmt.Printf("%d ", pw); pw *= 2 }; fmt.Println()
	fmt.Print("  range:   "); for i, v := range []string{"Go", "is", "fast"} { fmt.Printf("[%d]%s ", i, v) }; fmt.Println()
	fmt.Print("  runes:   "); for i, r := range "Go!" { fmt.Printf("[%d]%c ", i, r) }; fmt.Println()
	sub("break/continue")
	fmt.Print("  odds<8:  ")
	for i := 1; i < 10; i++ { if i >= 8 { break }; if i%2 == 0 { continue }; fmt.Printf("%d ", i) }
	fmt.Println()
	sub("switch")
	for _, score := range []int{95, 83, 72, 45} {
		switch {
		case score >= 90: fmt.Printf("  %d->A ", score)
		case score >= 80: fmt.Printf("  %d->B ", score)
		case score >= 70: fmt.Printf("  %d->C ", score)
		default:          fmt.Printf("  %d->F ", score)
		}
	}
	fmt.Println()
	sub("fallthrough")
	switch 2 {
	case 1: fmt.Println("  one"); fallthrough
	case 2: fmt.Println("  two"); fallthrough
	case 3: fmt.Println("  three (via fallthrough)")
	}

	section(9, "Arrays — fixed-size value types")
	primes := [...]int{2, 3, 5, 7, 11, 13, 17, 19}
	fmt.Printf("  primes: %v  len=%d\n", primes, len(primes))
	cpy := primes; cpy[0] = 999
	fmt.Printf("  original[0]=%d  copy[0]=%d  (value type!)\n", primes[0], cpy[0])
	var mat [3][3]int
	for i := range mat { for j := range mat[i] { mat[i][j] = i*3 + j + 1 } }
	fmt.Printf("  matrix: %v\n", mat)

	section(10, "Slices — dynamic arrays")
	sl := []int{1, 2, 3, 4, 5}; sl = append(sl, 6, 7, 8)
	fmt.Printf("  after append: %v  len=%d  cap=%d\n", sl, len(sl), cap(sl))
	dst := make([]int, 4); n3 := copy(dst, sl)
	fmt.Printf("  copy(4) -> %v  n=%d\n", dst, n3)
	fmt.Printf("  sl[2:5]=%v  sl[:3]=%v  sl[5:]=%v\n", sl[2:5], sl[:3], sl[5:])
	people := []struct{ Name string; Age int }{
		{"Charlie", 35}, {"Alice", 28}, {"Bob", 42}, {"Diana", 31},
	}
	sort.Slice(people, func(i, j int) bool { return people[i].Age < people[j].Age })
	fmt.Print("  sort by age: ")
	for _, p3 := range people { fmt.Printf("%s(%d) ", p3.Name, p3.Age) }
	fmt.Println()
	var stack []int
	for _, v := range []int{10, 20, 30} { stack = append(stack, v) }
	top := stack[len(stack)-1]; stack = stack[:len(stack)-1]
	fmt.Printf("  stack push 10,20,30 -> pop: top=%d  remaining=%v\n", top, stack)

	section(11, "Maps")
	freq := make(map[string]int)
	for _, w := range strings.Fields("the quick brown fox jumps over the lazy dog the fox") {
		freq[w]++
	}
	keys := make([]string, 0, len(freq))
	for k := range freq { keys = append(keys, k) }
	sort.Strings(keys)
	for _, k := range keys { fmt.Printf("  %-10s %d\n", k, freq[k]) }
	m := map[string]int{"go": 1, "python": 2, "rust": 3}
	if v, ok := m["go"]; ok { fmt.Printf("  go=%d\n", v) }
	if _, ok := m["java"]; !ok { fmt.Println("  java: not found") }

	section(12, "Functions")
	r4, err := divide(10, 3)
	if err == nil { fmt.Printf("  10/3=%.4f\n", r4) }
	_, err = divide(5, 0)
	fmt.Printf("  5/0 error: %v\n", err)
	lo, hi := minMax([]int{5, 1, 9, 3, 7, 4, 8, 2, 6})
	fmt.Printf("  minMax -> min=%d  max=%d\n", lo, hi)
	sumFn := func(ns ...int) int { t := 0; for _, n := range ns { t += n }; return t }
	fmt.Printf("  sum(1..5)=%d  sum(spread...)=%d\n",
		sumFn(1, 2, 3, 4, 5), sumFn([]int{10, 20, 30, 40}...))
	doubled := MapSlice([]int{1, 2, 3, 4, 5}, func(n int) int { return n * 2 })
	fmt.Printf("  MapSlice(double): %v\n", doubled)

	section(13, "Closures")
	ctr := makeCounter(0)
	fmt.Printf("  counter: %d %d %d %d %d\n", ctr(), ctr(), ctr(), ctr(), ctr())
	triple2 := makeMultiplier(3)
	fmt.Printf("  x3: %.0f %.0f %.0f\n", triple2(2), triple2(5), triple2(10))
	callCount := 0
	memo := func() func(int) int {
		cache := make(map[int]int)
		return func(n int) int {
			callCount++
			if v, ok := cache[n]; ok { callCount--; return v }
			r := n * n; cache[n] = r; return r
		}
	}()
	fmt.Printf("  memo(7)=%d  memo(7)=%d  actual_calls=%d\n", memo(7), memo(7), callCount)
	funcs := make([]func() int, 5)
	for i := 0; i < 5; i++ { i := i; funcs[i] = func() int { return i * i } }
	fmt.Print("  squares (fixed loop capture): ")
	for _, f := range funcs { fmt.Printf("%d ", f()) }
	fmt.Println()

	section(14, "Pointers")
	val := 42; ptr := &val
	fmt.Printf("  val=%d  ptr=%p  *ptr=%d\n", val, ptr, *ptr)
	*ptr = 100
	fmt.Printf("  after *ptr=100: val=%d\n", val)
	pt := Point{3, 4}
	fmt.Printf("  before Scale: %v\n", pt)
	pt.Scale(2)
	fmt.Printf("  after Scale(2): %v\n", pt)

	section(15, "Structs & Embedding")
	alice := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}
	alice.Birthday()
	fmt.Printf("  %v\n", alice)
	rex := Dog{Animal: Animal{Name: "Rex", Sound: "Woof"}, Breed: "Labrador"}
	fmt.Printf("  rex.Name=%q  rex.Breed=%q\n", rex.Name, rex.Breed)
	fmt.Printf("  Animal.Speak(): %s\n", rex.Animal.Speak())
	fmt.Printf("  Dog.Speak():    %s\n", rex.Speak())

	section(16, "Methods")
	c2 := Circle{Radius: 5}
	fmt.Printf("  Circle r=5: area=%.4f  perim=%.4f\n", c2.Area(), c2.Perimeter())
	r2 := Rect{W: 10, H: 4}
	fmt.Printf("  Rect 10x4: area=%.0f  perim=%.0f\n", r2.Area(), r2.Perimeter())
	o := Point{0, 0}; p3 := Point{3, 4}
	fmt.Printf("  distance %v->%v = %.2f\n", o, p3, o.Distance(p3))

	section(17, "Interfaces")
	shapes := []Shape{Circle{Radius: 5}, Rect{W: 10, H: 4}, Triangle{A: 3, B: 4, C: 5}}
	total := 0.0
	for _, sh := range shapes {
		fmt.Printf("  %-20T area=%8.4f  perim=%8.4f\n", sh, sh.Area(), sh.Perimeter())
		total += sh.Area()
	}
	fmt.Printf("  total area = %.4f\n", total)
	var iface interface{} = Circle{Radius: 3}
	if c3, ok := iface.(Circle); ok {
		fmt.Printf("  asserted Circle: r=%.0f  area=%.4f\n", c3.Radius, c3.Area())
	}
	for _, v := range []interface{}{42, 3.14, "go", true, Circle{Radius: 1}} {
		switch tv := v.(type) {
		case int:     fmt.Printf("  int: %d\n", tv)
		case float64: fmt.Printf("  float64: %g\n", tv)
		case string:  fmt.Printf("  string: %q\n", tv)
		case bool:    fmt.Printf("  bool: %t\n", tv)
		case Shape:   fmt.Printf("  Shape area=%.4f\n", tv.Area())
		}
	}

	section(18, "Error Handling")
	for _, inp := range []string{"42", "-1", "abc", "100"} {
		n5, e5 := safeParse(inp)
		if e5 != nil {
			var pe *ParseError
			if errors.As(e5, &pe)         { fmt.Printf("  ParseError: input=%q\n", pe.Input); continue }
			if errors.Is(e5, ErrNegative) { fmt.Printf("  ErrNegative: %q\n", inp); continue }
			fmt.Printf("  error: %v\n", e5)
		} else {
			fmt.Printf("  safeParse(%q) = %d\n", inp, n5)
		}
	}
	base := errors.New("base"); wrapped := fmt.Errorf("context: %w", base)
	fmt.Printf("  errors.Is(wrapped, base) = %t\n", errors.Is(wrapped, base))

	section(19, "defer / panic / recover")
	func() {
		defer fmt.Println("  deferred A (runs 3rd)")
		defer fmt.Println("  deferred B (runs 2nd)")
		defer fmt.Println("  deferred C (runs 1st)")
		fmt.Println("  function body")
	}()
	res, err2 := safeDiv(10, 0)
	fmt.Printf("  safeDiv(10,0): result=%v  err=%v\n", res, err2)
	res, _ = safeDiv(10, 4)
	fmt.Printf("  safeDiv(10,4): result=%.2f\n", res)

	section(20, "Goroutines")
	var wg sync.WaitGroup
	results := make([]int, 6)
	for i := 0; i < 6; i++ {
		wg.Add(1)
		go func(id int) { defer wg.Done(); results[id] = id * id }(i)
	}
	wg.Wait()
	fmt.Printf("  goroutine squares: %v\n", results)
	var counter int64
	var wg2 sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg2.Add(1)
		go func() { defer wg2.Done(); atomic.AddInt64(&counter, 1) }()
	}
	wg2.Wait()
	fmt.Printf("  atomic counter after 1000 goroutines: %d\n", counter)

	section(21, "Channels & select")
	fmt.Print("  pipeline squares: ")
	for v := range squarePipe(genNums(1, 2, 3, 4, 5, 6, 7, 8)) {
		fmt.Printf("%d ", v)
	}
	fmt.Println()
	buf := make(chan string, 4)
	for _, v := range []string{"one", "two", "three", "four"} { buf <- v }
	close(buf)
	fmt.Print("  buffered: ")
	for v := range buf { fmt.Printf("%s ", v) }
	fmt.Println()
	ch1 := make(chan int, 1); ch2 := make(chan int, 1); ch1 <- 42
	select {
	case v := <-ch1: fmt.Printf("  select ch1: %d\n", v)
	case v := <-ch2: fmt.Printf("  select ch2: %d\n", v)
	}
	slowCh := make(chan string, 1)
	go func() { time.Sleep(1 * time.Millisecond); slowCh <- "done" }()
	select {
	case v := <-slowCh:                fmt.Printf("  timeout pattern: got %q\n", v)
	case <-time.After(2 * time.Second): fmt.Println("  timed out")
	}

	section(22, "sync Package")
	var mu sync.Mutex; safeCtr := 0; var wg3 sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg3.Add(1)
		go func() { defer wg3.Done(); mu.Lock(); safeCtr++; mu.Unlock() }()
	}
	wg3.Wait()
	fmt.Printf("  mutex counter = %d\n", safeCtr)
	var once sync.Once
	for i := 0; i < 5; i++ {
		once.Do(func() { fmt.Println("  sync.Once: initialised exactly once!") })
	}

	section(23, "Standard Library Highlights")
	s2 := "the quick brown fox"
	fmt.Printf("  ToUpper: %q\n", strings.ToUpper(s2))
	fmt.Printf("  Fields:  %v\n", strings.Fields(s2))
	fmt.Printf("  Join:    %q\n", strings.Join([]string{"Go", "is", "great"}, " "))
	fmt.Printf("  Replace: %q\n", strings.ReplaceAll(s2, "o", "0"))
	ints5 := []int{3, 1, 4, 1, 5, 9, 2, 6}; sort.Ints(ints5)
	fmt.Printf("  sort.Ints:    %v\n", ints5)
	fmt.Printf("  SearchInts(5): idx=%d\n", sort.SearchInts(ints5, 5))
	fmt.Printf("  Sqrt(2)=%.6f  Pow(2,32)=%.0f  Pi=%.10f\n",
		math.Sqrt(2), math.Pow(2, 32), math.Pi)
	now := time.Now()
	fmt.Printf("  time.Now: %s  Weekday: %s\n",
		now.Format("2006-01-02 15:04:05"), now.Weekday())

	section(24, "Generics (Go 1.18+)")
	fmt.Printf("  Min(3,7)=%d  Min(\"apple\",\"banana\")=%q\n",
		Min(3, 7), Min("apple", "banana"))
	fmt.Printf("  Max(3.14,2.72)=%.2f\n", Max(3.14, 2.72))
	fmt.Printf("  SumSlice(ints)=%d  SumSlice(floats)=%.1f\n",
		SumSlice([]int{1, 2, 3, 4, 5}), SumSlice([]float64{1.1, 2.2, 3.3}))
	fmt.Printf("  MapSlice(x2):   %v\n",
		MapSlice([]int{1, 2, 3, 4, 5}, func(n int) int { return n * 2 }))
	fmt.Printf("  MapSlice(Itoa): %v\n", MapSlice([]int{1, 2, 3}, strconv.Itoa))
	fmt.Printf("  FilterSlice(even): %v\n",
		FilterSlice([]int{1, 2, 3, 4, 5, 6, 7, 8}, func(n int) bool { return n%2 == 0 }))
	fmt.Printf("  Reduce(+): %d\n",
		Reduce([]int{1, 2, 3, 4, 5}, 0, func(acc, v int) int { return acc + v }))
	var stk Stack[string]
	for _, w := range []string{"Go", "is", "awesome"} { stk.Push(w) }
	fmt.Printf("  Stack len=%d  peek=", stk.Len())
	if v, ok := stk.Peek(); ok { fmt.Printf("%q\n", v) }
	fmt.Print("  pop order: ")
	for stk.Len() > 0 { if v, ok := stk.Pop(); ok { fmt.Printf("%q ", v) } }
	fmt.Println()

	section(25, "Best Practices")
	fmt.Println("  ok  Always check errors")
	fmt.Println("  ok  Accept interfaces, return concrete types")
	fmt.Println("  ok  Use defer for cleanup (Close, Unlock, etc.)")
	fmt.Println("  ok  Wrap errors: fmt.Errorf(\"context: %w\", err)")
	fmt.Println("  ok  Keep interfaces small, prefer 1-method interfaces")
	fmt.Println("  ok  Use sync/atomic for counters, sync.Mutex for structs")
	fmt.Println("  ok  Run: go fmt && go vet && go test -race")
	fmt.Println("  no  Don't panic in library code")
	fmt.Println("  no  Don't start goroutines without a clear exit path")
	fmt.Println("  no  Don't use interface{} when generics work (1.18+)")

	fmt.Printf("\n%s\n  Tutorial complete! %d sections demonstrated.\n%s\n",
		strings.Repeat("=", 60), 23, strings.Repeat("=", 60))
}
sample output (excerpt)============================================================ §04 Variables & Zero Values ============================================================ zeros: int=0 float64=0 string="" bool=false *int=<nil> short: x=42 y=3.14 name="Gopher" swap: a=200 b=100 ... ============================================================ §24 Generics (Go 1.18+) ============================================================ Min(3,7)=3 Min("apple","banana")="apple" Max(3.14,2.72)=3.14 SumSlice(ints)=15 SumSlice(floats)=6.6 MapSlice(x2): [2 4 6 8 10] MapSlice(Itoa): [1 2 3] FilterSlice(even): [2 4 6 8] Reduce(+): 15 Stack len=3 peek="awesome" pop order: "awesome" "is" "Go" ============================================================ Tutorial complete! 23 sections demonstrated. ============================================================