Go Concurrency

Concurrency is one of Go's most powerful features, built into the language from the ground up. Go provides goroutines and channels as first-class language constructs that make concurrent programming simple and efficient. Understanding Go's concurrency patterns is essential for building high-performance, scalable applications.

What is Concurrency

Concurrency is the ability to handle multiple tasks by switching between them, not necessarily executing them simultaneously. Go's concurrency model is based on Communicating Sequential Processes (CSP), which emphasizes passing messages between independent processes rather than sharing memory.

graph TD A[Main Program] --> B[Goroutine 1] A --> C[Goroutine 2] A --> D[Goroutine 3] B <-.Channel.-> C C <-.Channel.-> D B <-.Channel.-> D

Goroutines

Goroutines are lightweight threads managed by the Go runtime. They are much cheaper than operating system threads - thousands of goroutines can run concurrently in a single program.

Creating Goroutines

package main import ( "fmt" "time" ) func sayHello() { fmt.Println("Hello from goroutine") } func main() { // Start goroutine go sayHello() // Start anonymous function as goroutine go func() { fmt.Println("Anonymous goroutine") }() // Main must wait or goroutines won't execute time.Sleep(time.Second) fmt.Println("Main function") }

Goroutine Communication

func printNumbers() { for i := 1; i <= 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Printf("%d ", i) } } func printLetters() { for i := 'a'; i < 'f'; i++ { time.Sleep(150 * time.Millisecond) fmt.Printf("%c ", i) } } func main() { go printNumbers() go printLetters() time.Sleep(2 * time.Second) fmt.Println("\nDone") }
sequenceDiagram participant Main participant Goroutine1 participant Goroutine2 Main->>Goroutine1: Start Main->>Goroutine2: Start Goroutine1->>Goroutine1: Execute Goroutine2->>Goroutine2: Execute Main->>Main: Continue

Channels

Channels are typed conduits for communication between goroutines. They provide a safe way to send and receive data.

Creating and Using Channels

// Create channel ch := make(chan int) // Send to channel ch <- 42 // Receive from channel value := <-ch // Buffered channel buffered := make(chan int, 3) // capacity of 3

Unbuffered Channels

Unbuffered channels block until both sender and receiver are ready.

func sendData(ch chan int) { fmt.Println("Sending data...") ch <- 42 fmt.Println("Data sent") } func main() { ch := make(chan int) go sendData(ch) fmt.Println("Waiting for data...") data := <-ch fmt.Println("Received:", data) }

Buffered Channels

Buffered channels allow sending multiple values without blocking until the buffer is full.

func main() { ch := make(chan int, 2) // buffer size 2 ch <- 1 // doesn't block ch <- 2 // doesn't block // ch <- 3 // would block until someone receives fmt.Println(<-ch) // 1 fmt.Println(<-ch) // 2 }

Channel Direction

Specify channel direction in function parameters for type safety.

// Send-only channel func sendOnly(ch chan<- int) { ch <- 42 } // Receive-only channel func receiveOnly(ch <-chan int) { value := <-ch fmt.Println(value) } func main() { ch := make(chan int) go sendOnly(ch) receiveOnly(ch) }

Channel Operations

Closing Channels

func producer(ch chan int) { for i := 0; i < 5; i++ { ch <- i } close(ch) // Signal no more values } func main() { ch := make(chan int) go producer(ch) // Range over channel until closed for value := range ch { fmt.Println(value) } // Check if channel is closed value, ok := <-ch if !ok { fmt.Println("Channel closed") } }

Select Statement

select allows waiting on multiple channel operations.

func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "from ch1" }() go func() { time.Sleep(2 * time.Second) ch2 <- "from ch2" }() // Wait for whichever is ready first select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) } // Select with default (non-blocking) select { case msg := <-ch1: fmt.Println(msg) default: fmt.Println("No data available") } }
graph TD A[Select Statement] --> B{Channel 1 Ready?} A --> C{Channel 2 Ready?} A --> D{Default Case?} B -->|Yes| E[Receive from Ch1] C -->|Yes| F[Receive from Ch2] D -->|None Ready| G[Execute Default]

Common Concurrency Patterns

Worker Pool Pattern

func worker(id int, jobs <-chan int, results chan<- int) { for job := range jobs { fmt.Printf("Worker %d processing job %d\n", id, job) time.Sleep(time.Second) results <- job * 2 } } func main() { jobs := make(chan int, 10) results := make(chan int, 10) // Start 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) // Collect results for a := 1; a <= 5; a++ { <-results } }

Fan-Out, Fan-In Pattern

// Fan-out: distribute work to multiple goroutines func fanOut(input <-chan int, workers int) []<-chan int { channels := make([]<-chan int, workers) for i := 0; i < workers; i++ { ch := make(chan int) channels[i] = ch go func(out chan<- int) { for value := range input { out <- value * 2 } close(out) }(ch) } return channels } // Fan-in: combine results from multiple channels func fanIn(channels ...<-chan int) <-chan int { out := make(chan int) for _, ch := range channels { go func(c <-chan int) { for value := range c { out <- value } }(ch) } return out }

Pipeline Pattern

// Stage 1: Generate numbers func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // Stage 2: Square numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // Stage 3: Print numbers func print(in <-chan int) { for n := range in { fmt.Println(n) } } func main() { // Pipeline: generate -> square -> print nums := generate(2, 3, 4, 5) squared := square(nums) print(squared) }
graph LR A[Generate] -->|Channel| B[Square] B -->|Channel| C[Print]

Synchronization Primitives

WaitGroup

WaitGroup waits for a collection of goroutines to finish.

import "sync" func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrement counter when done fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) // Increment counter go worker(i, &wg) } wg.Wait() // Block until counter is 0 fmt.Println("All workers done") }

Mutex

Mutex provides exclusive access to shared data.

import "sync" type Counter struct { mu sync.Mutex value int } func (c *Counter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.value++ } func (c *Counter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.value } func main() { counter := &Counter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment() }() } wg.Wait() fmt.Println("Counter:", counter.Value()) }

RWMutex

RWMutex allows multiple readers or one writer.

import "sync" type Cache struct { mu sync.RWMutex data map[string]string } func (c *Cache) Get(key string) string { c.mu.RLock() // Read lock defer c.mu.RUnlock() return c.data[key] } func (c *Cache) Set(key, value string) { c.mu.Lock() // Write lock defer c.mu.Unlock() c.data[key] = value }

Context Package

Context carries deadlines, cancellation signals, and request-scoped values.

import ( "context" "fmt" "time" ) func operation(ctx context.Context) error { // Check for cancellation select { case <-ctx.Done(): return ctx.Err() case <-time.After(2 * time.Second): fmt.Println("Operation complete") return nil } } func main() { // Context with timeout ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() if err := operation(ctx); err != nil { fmt.Println("Error:", err) } // Context with cancellation ctx2, cancel2 := context.WithCancel(context.Background()) go func() { time.Sleep(500 * time.Millisecond) cancel2() // Cancel the context }() if err := operation(ctx2); err != nil { fmt.Println("Cancelled:", err) } }
sequenceDiagram participant Main participant Context participant Operation Main->>Context: Create with timeout Main->>Operation: Start (pass context) Operation->>Context: Check Done channel Context->>Operation: Timeout signal Operation->>Main: Return error

Rate Limiting

import ( "fmt" "time" ) func main() { requests := make(chan int, 5) // Fill requests for i := 1; i <= 5; i++ { requests <- i } close(requests) // Rate limiter: 1 per 200ms limiter := time.Tick(200 * time.Millisecond) for req := range requests { <-limiter // Wait for tick fmt.Println("Request", req, time.Now()) } } // Bursty rate limiter func burstLimiter() { requests := make(chan int, 5) for i := 1; i <= 5; i++ { requests <- i } close(requests) // Allow bursts of 3 burstyLimiter := make(chan time.Time, 3) for i := 0; i < 3; i++ { burstyLimiter <- time.Now() } // Refill one per 200ms go func() { for t := range time.Tick(200 * time.Millisecond) { burstyLimiter <- t } }() for req := range requests { <-burstyLimiter fmt.Println("Request", req, time.Now()) } }

Practical Example: Concurrent Web Scraper

package main import ( "fmt" "io/ioutil" "net/http" "sync" "time" ) type Result struct { URL string Status int Size int Error error } func fetchURL(url string, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() client := http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { results <- Result{URL: url, Error: err} return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { results <- Result{URL: url, Error: err} return } results <- Result{ URL: url, Status: resp.StatusCode, Size: len(body), } } func main() { urls := []string{ "https://golang.org", "https://github.com", "https://stackoverflow.com", } results := make(chan Result, len(urls)) var wg sync.WaitGroup // Start workers for _, url := range urls { wg.Add(1) go fetchURL(url, results, &wg) } // Wait and close results go func() { wg.Wait() close(results) }() // Collect results for result := range results { if result.Error != nil { fmt.Printf("%s: Error - %v\n", result.URL, result.Error) } else { fmt.Printf("%s: Status %d, Size %d bytes\n", result.URL, result.Status, result.Size) } } }

Key Takeaways