The Go Programming
Language
27 sections from first program to generics and concurrency — each section matched to a verified, runnable tutorial program.
go_tutorial.go (at the bottom) produce exactly that output. Run go run go_tutorial.go and follow along.Foundations — The Language & Core Types
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
Entire programs compile in seconds. Binaries are statically linked — one file, no runtime dependencies.
Goroutines cost ~2 KB vs ~1 MB for OS threads. Channels make safe communication expressive.
Memory is managed automatically with a low-latency GC. No malloc/free, no RAII.
Static types with inference. No implicit conversion ever. Most bugs caught at compile time.
Only 25 keywords. One formatting style enforced by gofmt. One right way to do most things.
go fmt, go vet, go test, go doc — all built in, zero config.
The 25 keywords — all of them
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return varGo vs other languages
| Feature | Go | Python | C++ | Rust | Java |
|---|---|---|---|---|---|
| Compilation | Fast compiled | Interpreted | Slow compiled | Slow compiled | JVM bytecode |
| Memory mgmt | GC | GC | Manual/RAII | Ownership | GC |
| Concurrency | Goroutines+channels | asyncio/GIL | threads/async | async/threads | threads/virtual |
| Learning curve | Low–medium | Low | Very high | High | Medium |
| Deploy | Single static binary | Interpreter required | Shared libs | Static binary | JVM required |
| Generics | Since 1.18 | Duck typing | Templates | Yes | Yes |
Installation & Setup
# 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/amd64mkdir ~/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 binarygo 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 variablesgolang.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.Hello, World!
// 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)
}fmt format verbs — complete reference
| Verb | What it formats | Example output |
|---|---|---|
%v | Default format for any type | {Alice 30} |
%+v | Struct with field names | {Name:Alice Age:30} |
%#v | Go syntax representation | main.Person{Name:"Alice"} |
%T | Type of the value | main.Person |
%d | Integer, decimal | 42 |
%b | Integer, binary | 101010 |
%o | Integer, octal | 52 |
%x | Integer, hex lowercase | 2a |
%08d | Zero-padded width 8 | 00000042 |
%-10d | Left-justified width 10 | 42 |
%f | Float, decimal notation | 3.140000 |
%.2f | Float, 2 decimal places | 3.14 |
%e | Float, scientific notation | 3.14e+00 |
%g | Float, shortest representation | 3.14 |
%s | String, unquoted | hello |
%q | String, double-quoted | "hello" |
%c | Character (rune) | A |
%t | Boolean | true |
%p | Pointer address | 0xc000012080 |
%w | Error wrap (only in fmt.Errorf) | (wraps the error) |
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.
// 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 value quick reference
| Type | Zero Value | Notes |
|---|---|---|
int, float64, etc. | 0 | All numeric types |
string | "" | Empty string, not nil |
bool | false | |
| pointer, func, interface | nil | Safe to compare with nil |
| slice, map, chan | nil | nil slice/chan usable; writing to nil map panics — use make() |
| struct | each field zeroed | Recursive zero values |
array [N]T | [N] zero values | Fully initialised, ready to use |
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.Types
Numeric types
| Type | Size | Range / Value | Use when |
|---|---|---|---|
int | Platform (64-bit on 64-bit OS) | −2⁶³ to 2⁶³−1 | General integer — default choice |
int8 int16 int32 int64 | 1–8 bytes | Explicit widths | Specific size needed (network, files) |
uint uint8 uint16 uint32 uint64 | 1–8 bytes | 0 to 2ⁿ−1 | Non-negative values only |
float32 | 4 bytes | ~±3.4×10³⁸, 7 digits | Graphics, when space matters |
float64 | 8 bytes | ~±1.8×10³⁰⁸, 15 digits | General float — default choice |
complex64/128 | 8/16 bytes | 3+4i | Complex math; use real(), imag() |
byte | 1 byte | 0–255 | Alias for uint8; raw bytes |
rune | 4 bytes | 0–1,114,111 | Alias for int32; Unicode code point |
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][,][ ]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.
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"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)Constants & iota
// 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) // trueconst 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.Control Flow & Collections
Control Flow — if, for, switch
if — with initialiser statement
// 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 leaksfor — Go's only loop (replaces while and do-while)
// 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)
}
}switch
// 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
}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.
// 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 }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.
// 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// 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 valueappend 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).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.
// 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) }sync.RWMutex around your map, or use sync.Map for read-heavy workloads.Functions, Methods & Types
Functions
// 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 = 9Closures
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.
// 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)
}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.
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 dereferenceStructs & Embedding
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
}// 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)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.
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 receiver | Method 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 receiver | You want a snapshot of the value at the time of call |
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.
// 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// 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 ANYTHINGErrors, Concurrency & Synchronisation
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.
// 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
}Error handling pattern reference
| Pattern | When to use |
|---|---|
return err | Propagate 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 |
defer · panic · recover
// 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// 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 crashingGoroutines
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 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)done channel or context.Context for cancellation.Channels & select
Channels are typed conduits for safe goroutine communication. Go's concurrency mantra: "Do not communicate by sharing memory; share memory by communicating."
// 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
}// 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 4sync Package
// ── 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) // CASEcosystem, Generics & Best Practices
Standard Library Highlights
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 item4import ("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 deadlineGenerics (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.
// 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)// 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")Testing with go test
Go has a built-in testing framework. Test files end in _test.go and are excluded from normal builds.
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
}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 cachePackages & Modules
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// 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 with | Visibility | Examples |
|---|---|---|
| Uppercase | Exported — accessible from any package | Println, Error, Config, UserID |
| Lowercase | Unexported — this package only | parse, internalState, helper |
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
| Thing | Style | Examples |
|---|---|---|
| Variables, functions | camelCase | numItems, parseConfig, isValid |
| Exported symbols | PascalCase | ReadFile, UserID, HTTPClient |
| Acronyms | All caps | userID not userId; parseURL not parseUrl |
| Single-method interfaces | -er suffix | Reader, Stringer, Handler, Logger |
| Packages | lowercase, short | http, fmt, strconv, mathutil |
| Sentinel errors | ErrXxx | ErrNotFound, 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
deferfor 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
panicin 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
// 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() immediatelyComplete 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.
# Download and run
go run go_tutorial.go
# Or build a permanent binary
go build -o go_tutorial go_tutorial.go && ./go_tutorial// =============================================================================
// 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))
}