>_ Golang Step By Step
Software Engineer

Concurrency

Goroutines, channels, and concurrent patterns in Go

# Goroutines

A goroutine is a lightweight thread managed by the Go runtime. Start one with the go keyword:

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    fmt.Println("Hello,", name)
}

func main() {
    go sayHello("Alice")  // runs concurrently
    go sayHello("Bob")    // runs concurrently

    time.Sleep(100 * time.Millisecond)
    fmt.Println("done")
}

⚠️ Don't rely on time.Sleep — use channels or sync.WaitGroup to synchronize.

# Channels

Channels are Go's primary mechanism for goroutine communication. They're typed conduits through which you send and receive values:

// Unbuffered channel
ch := make(chan string)

go func() {
    ch <- "hello"  // send
}()

msg := <-ch              // receive (blocks until sent)
fmt.Println(msg)         // hello

Buffered Channels

// Buffered: holds up to 3 values without blocking
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block (buffer full)

fmt.Println(<-ch) // 1 (FIFO)
fmt.Println(<-ch) // 2

Range over Channel

ch := make(chan int)
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // must close for range to exit
}()

for v := range ch {
    fmt.Println(v)
}

# select — Multiplexing Channels

select waits on multiple channel operations and executes the first one that's ready:

select {
case msg := <-ch1:
    fmt.Println("from ch1:", msg)
case msg := <-ch2:
    fmt.Println("from ch2:", msg)
case <-time.After(1 * time.Second):
    fmt.Println("timeout!")
default:
    fmt.Println("nothing ready")
}

# sync.WaitGroup & sync.Mutex

import "sync"

func main() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    total := 0

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            mu.Lock()
            total += n
            mu.Unlock()
        }(i)
    }

    wg.Wait() // block until all goroutines done
    fmt.Println(total) // 15
}

# Pattern: Fan-out / Fan-in

A common concurrency pattern: fan-out work to multiple goroutines, fan-in results through a single channel:

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d processing job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan int, 10)

    // Fan-out: 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Fan-in: collect results
    for r := 1; r <= 5; r++ {
        fmt.Println(<-results)
    }
}

⚡ Key Takeaways

  • Goroutines are extremely cheap — spawn thousands without worry
  • Channels synchronize goroutines — unbuffered channels block until both sides are ready
  • select multiplexes channels — great for timeouts and cancellation
  • Use sync.WaitGroup to wait for goroutine completion
  • Use sync.Mutex or channels for shared state — never access shared data without synchronization
  • Use go run -race to detect race conditions
practice & review