Skip to content

Go Lesson Plan

A progressive curriculum to master Go through hands-on practice.

Goal: Run Go and understand basic types.

Go is statically typed, compiled, and emphasizes simplicity. Every Go file belongs to a package. main is the entry point.

  1. Create and run a program

    main.go
    package main
    import "fmt"
    func main() {
    fmt.Println("Hello, World!")
    }
    Terminal window
    go run main.go
    go build main.go && ./main
  2. Variables and types

    // Explicit type
    var name string = "Go"
    var age int = 10
    // Type inference
    count := 42
    pi := 3.14
    // Constants
    const MaxSize = 100
  3. Basic types

    // Numbers
    var i int = 42
    var f float64 = 3.14
    var c complex128 = 1 + 2i
    // Strings
    s := "hello"
    len(s) // 5
    s[0] // 104 (byte value of 'h')
    s + " world" // "hello world"
    // Booleans
    var flag bool = true
  4. Zero values

    var i int // 0
    var f float64 // 0.0
    var s string // ""
    var b bool // false
    var p *int // nil

Write a program that declares variables of different types and prints them.


Goal: Use conditionals and loops.

Go has only one loop construct: for. No parentheses around conditions. Braces are required.

  1. If statements

    x := 10
    if x > 0 {
    fmt.Println("positive")
    } else if x < 0 {
    fmt.Println("negative")
    } else {
    fmt.Println("zero")
    }
    // With initialization
    if err := doSomething(); err != nil {
    fmt.Println(err)
    }
  2. For loops

    // Traditional
    for i := 0; i < 5; i++ {
    fmt.Println(i)
    }
    // While-style
    n := 0
    for n < 5 {
    fmt.Println(n)
    n++
    }
    // Infinite
    for {
    break // Exit
    }
    // Range
    nums := []int{1, 2, 3}
    for i, v := range nums {
    fmt.Printf("%d: %d
    ", i, v)
    }
  3. Switch

    day := "Monday"
    switch day {
    case "Monday":
    fmt.Println("Start of week")
    case "Friday":
    fmt.Println("Almost weekend")
    default:
    fmt.Println("Regular day")
    }
    // No condition (like if-else chain)
    switch {
    case x < 0:
    fmt.Println("negative")
    case x > 0:
    fmt.Println("positive")
    default:
    fmt.Println("zero")
    }
  4. Defer

    func main() {
    defer fmt.Println("cleanup") // Runs at function exit
    fmt.Println("working")
    }
    // Output: working, cleanup

Write a FizzBuzz program using a for loop and switch.


Goal: Work with arrays, slices, and maps.

  1. Arrays (fixed size)

    var arr [3]int // [0, 0, 0]
    arr[0] = 1
    len(arr) // 3
    arr2 := [3]int{1, 2, 3}
    arr3 := [...]int{1, 2, 3} // Size inferred
  2. Slices (dynamic)

    // Create
    s := []int{1, 2, 3}
    s2 := make([]int, 3) // [0, 0, 0]
    s3 := make([]int, 0, 10) // len=0, cap=10
    // Append
    s = append(s, 4, 5)
    // Slice operations
    s[1:3] // [2, 3]
    s[:2] // [1, 2]
    s[2:] // [3, 4, 5]
    // Copy
    dst := make([]int, len(s))
    copy(dst, s)
  3. Maps

    // Create
    m := map[string]int{
    "alice": 30,
    "bob": 25,
    }
    m2 := make(map[string]int)
    // Access
    age := m["alice"]
    age, ok := m["unknown"] // ok = false if missing
    // Modify
    m["charlie"] = 35
    delete(m, "bob")
    // Iterate
    for key, value := range m {
    fmt.Printf("%s: %d
    ", key, value)
    }
  4. Nil slices and maps

    var s []int // nil, len=0, can append
    var m map[string]int // nil, cannot write (panic)
    m = make(map[string]int) // Now safe to write

Create a word frequency counter using a map.


Goal: Define functions and understand multiple returns.

  1. Basic functions

    func add(a int, b int) int {
    return a + b
    }
    // Same-type parameters
    func add2(a, b int) int {
    return a + b
    }
  2. Multiple return values

    func divide(a, b float64) (float64, error) {
    if b == 0 {
    return 0, errors.New("division by zero")
    }
    return a / b, nil
    }
    result, err := divide(10, 2)
    if err != nil {
    log.Fatal(err)
    }
  3. Named returns

    func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // Returns x and y
    }
  4. Variadic and closures

    // Variadic
    func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
    total += n
    }
    return total
    }
    sum(1, 2, 3, 4) // 10
    // Closure
    func counter() func() int {
    count := 0
    return func() int {
    count++
    return count
    }
    }

Write a function that returns both the min and max of a slice.


Goal: Define types with methods.

  1. Structs

    type Person struct {
    Name string
    Age int
    }
    // Create
    p1 := Person{Name: "Alice", Age: 30}
    p2 := Person{"Bob", 25} // Positional
    p3 := new(Person) // *Person, zero values
    // Access
    p1.Name = "Alicia"
  2. Methods

    type Rectangle struct {
    Width, Height float64
    }
    // Value receiver (copy)
    func (r Rectangle) Area() float64 {
    return r.Width * r.Height
    }
    // Pointer receiver (mutate)
    func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
    }
    rect := Rectangle{10, 5}
    rect.Area() // 50
    rect.Scale(2) // Now 20x10
  3. Embedding

    type Animal struct {
    Name string
    }
    func (a Animal) Speak() string {
    return "..."
    }
    type Dog struct {
    Animal // Embedded
    Breed string
    }
    d := Dog{Animal{"Rex"}, "Labrador"}
    d.Name // "Rex" (promoted field)
    d.Speak() // "..." (promoted method)
  4. Struct tags

    type User struct {
    Name string `json:"name"`
    Email string `json:"email,omitempty"`
    }

Create a Stack struct with Push and Pop methods.


Goal: Define behavior with interfaces.

Interfaces are satisfied implicitly. A type implements an interface by implementing its methods.

  1. Define and implement

    type Speaker interface {
    Speak() string
    }
    type Dog struct{ Name string }
    func (d Dog) Speak() string {
    return "Woof!"
    }
    var s Speaker = Dog{"Rex"}
    s.Speak() // "Woof!"
  2. Empty interface

    func printAny(v interface{}) {
    fmt.Println(v)
    }
    // Or with any (Go 1.18+)
    func printAny2(v any) {
    fmt.Println(v)
    }
  3. Type assertions

    var i interface{} = "hello"
    s := i.(string) // Panics if wrong type
    s, ok := i.(string) // ok = false if wrong
    // Type switch
    switch v := i.(type) {
    case string:
    fmt.Println("string:", v)
    case int:
    fmt.Println("int:", v)
    default:
    fmt.Println("unknown")
    }
  4. Common interfaces

    io.Reader
    type Reader interface {
    Read(p []byte) (n int, err error)
    }
    // io.Writer
    type Writer interface {
    Write(p []byte) (n int, err error)
    }
    // error
    type error interface {
    Error() string
    }

Define a Shape interface with Area() method. Implement for Circle and Rectangle.


Goal: Use goroutines and channels.

  1. Goroutines

    func say(s string) {
    for i := 0; i < 3; i++ {
    fmt.Println(s)
    time.Sleep(100 * time.Millisecond)
    }
    }
    go say("hello") // Runs concurrently
    say("world") // Runs in main
  2. Channels

    ch := make(chan int)
    go func() {
    ch <- 42 // Send
    }()
    value := <-ch // Receive (blocks)
    fmt.Println(value)
    // Buffered channel
    ch2 := make(chan int, 3)
  3. Select

    ch1 := make(chan string)
    ch2 := make(chan string)
    go func() { ch1 <- "one" }()
    go func() { ch2 <- "two" }()
    select {
    case msg := <-ch1:
    fmt.Println(msg)
    case msg := <-ch2:
    fmt.Println(msg)
    case <-time.After(1 * time.Second):
    fmt.Println("timeout")
    }
  4. WaitGroup

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
    defer wg.Done()
    fmt.Println(n)
    }(i)
    }
    wg.Wait() // Block until all done

Create a worker pool with 3 workers processing jobs from a channel.


Goal: Handle errors idiomatically and write tests.

  1. Error handling

    func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
    return nil, fmt.Errorf("reading %s: %w", path, err)
    }
    return data, nil
    }
    // Check error
    data, err := readFile("config.json")
    if err != nil {
    log.Fatal(err)
    }
  2. Custom errors

    type ValidationError struct {
    Field string
    Message string
    }
    func (e ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
    }
    // errors.Is and errors.As
    if errors.Is(err, os.ErrNotExist) {
    // Handle missing file
    }
    var valErr ValidationError
    if errors.As(err, &valErr) {
    fmt.Println(valErr.Field)
    }
  3. Writing tests

    add_test.go
    package main
    import "testing"
    func TestAdd(t *testing.T) {
    got := add(2, 3)
    want := 5
    if got != want {
    t.Errorf("add(2, 3) = %d; want %d", got, want)
    }
    }
    // Run: go test
  4. Table-driven tests

    func TestAdd(t *testing.T) {
    tests := []struct {
    a, b, want int
    }{
    {1, 2, 3},
    {0, 0, 0},
    {-1, 1, 0},
    }
    for _, tt := range tests {
    t.Run(fmt.Sprintf("%d+%d", tt.a, tt.b), func(t *testing.T) {
    got := add(tt.a, tt.b)
    if got != tt.want {
    t.Errorf("got %d, want %d", got, tt.want)
    }
    })
    }
    }

Write a function with custom error type and table-driven tests.


Build a command-line file utility:

  • Use flag package for arguments
  • Read/write files
  • Handle errors gracefully

Build a REST API:

  • Use net/http
  • JSON encoding/decoding
  • Middleware pattern

Fetch multiple URLs concurrently:

  • Use goroutines and channels
  • Implement timeout
  • Aggregate results

StageTopics
BeginnerTypes, control flow, functions, collections
IntermediateStructs, methods, interfaces, error handling
AdvancedGoroutines, channels, testing, reflection