Go Gopher How to Go

Goroutines are lightweight threads managed by the Go runtime. They enable concurrent execution of functions, making it easy to write programs that do multiple things at once. Goroutines are one of Go's most powerful features.

💡 Key Points

  • Goroutines are lightweight - thousands can run simultaneously
  • Start a goroutine with the go keyword
  • Goroutines run in the same address space
  • The main function runs in its own goroutine
  • Program exits when main goroutine completes
  • Use channels or sync primitives for coordination
  • Goroutines are multiplexed onto OS threads
  • Much cheaper than OS threads (few KB of stack)

Starting Goroutines

Use the go keyword to run a function concurrently:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 3; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    // Start goroutine
    go say("world")
    
    // Main goroutine continues
    say("hello")
    
    // Give goroutines time to finish
    time.Sleep(500 * time.Millisecond)
    fmt.Println("done")
}
Output:
hello
world
hello
world
hello
world
done

Goroutines with Anonymous Functions

Commonly use anonymous functions with goroutines:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Launch multiple goroutines
    for i := 0; i < 3; i++ {
        go func(id int) {
            fmt.Printf("Goroutine %d starting\n", id)
            time.Sleep(time.Millisecond * 100)
            fmt.Printf("Goroutine %d done\n", id)
        }(i) // Pass i as argument to avoid closure issues
    }
    
    // Wait for goroutines to finish
    time.Sleep(time.Second)
    fmt.Println("All done")
}
Output:
Goroutine 0 starting
Goroutine 1 starting
Goroutine 2 starting
Goroutine 0 done
Goroutine 1 done
Goroutine 2 done
All done

WaitGroup for Synchronization

Use sync.WaitGroup to wait for goroutines to complete:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Decrement counter when done
    
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Millisecond * 100)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    // Start 5 workers
    for i := 1; i <= 5; i++ {
        wg.Add(1) // Increment counter
        go worker(i, &wg)
    }
    
    // Wait for all goroutines to finish
    wg.Wait()
    fmt.Println("All workers completed")
}
Output:
Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed

Goroutine Syntax

PatternExampleDescription
Start goroutinego function()Run function concurrently
Anonymous functiongo func() { ... }()Inline goroutine
With argumentsgo func(x int) { ... }(42)Pass values to goroutine
Method callgo obj.Method()Run method concurrently

Synchronization Primitives

TypePurposeUse Case
sync.WaitGroupWait for collection of goroutinesParallel task completion
sync.MutexMutual exclusion lockProtect shared data
sync.RWMutexReader/writer lockMultiple readers, single writer
sync.OnceExecute action exactly onceInitialization, singleton
sync.CondCondition variableWait for/signal events
ChannelsCommunication between goroutinesData exchange, synchronization

Goroutine Characteristics

FeatureDescriptionDetails
LightweightMuch cheaper than OS threadsStart with ~2KB stack, grows as needed
MultiplexedMany goroutines on few OS threadsGo runtime manages scheduling
Fast startupCreation is very fastFaster than thread creation
Shared memoryAccess same address spaceNeed synchronization for safety
Non-deterministicExecution order not guaranteedUse channels for coordination
Automatic cleanupRuntime handles lifecycleNo manual thread management

Common Patterns

PatternCodeUse Case
Fire and forgetgo function()Background task, no result needed
Worker poolfor i := 0; i < N; i++ { go worker() }Parallel processing
Fan-outOne input, many workersDistribute work
Fan-inMany inputs, one outputCollect results
PipelineChain of processing stagesData transformation
Timeoutselect with timerBound goroutine execution time

Best Practices

PracticeDescriptionReason
Pass data by valueUse function parametersAvoid closure variable issues
Use WaitGroupCoordinate goroutine completionBetter than time.Sleep
Avoid goroutine leaksEnsure goroutines can exitPrevent resource exhaustion
Use channels for communicationShare by communicatingSafer than shared memory
Limit concurrencyDon't spawn unlimited goroutinesUse worker pools
Handle errors properlyReturn errors via channelsDon't panic in goroutines
Use context for cancellationPass context.ContextGraceful shutdown

Common Pitfalls

PitfallProblemSolution
Closure variable captureLoop variable sharedPass as parameter: go func(i int){...}(i)
Goroutine leakGoroutines never exitUse context or done channels
Race conditionsUnsynchronized data accessUse mutexes or channels
Early program exitMain exits before goroutinesUse WaitGroup or channels
Unbounded creationToo many goroutinesUse worker pools, semaphores
Panic in goroutineCrashes entire programUse defer/recover