Skip to main content

Go Goroutines

Goroutines are the fundamental building blocks of concurrent programming in Go. They are lightweight threads that enable you to run multiple functions concurrently without the overhead of traditional operating system threads. Understanding goroutines is crucial for writing efficient, scalable Go programs that can handle multiple tasks simultaneously. This comprehensive guide will teach you everything you need to know about creating, managing, and working with goroutines in Go.

Understanding Goroutines

What Are Goroutines?

Goroutines are lightweight threads managed by the Go runtime that enable concurrent execution of functions. They provide:

  • Lightweight execution - Much lighter than OS threads (2KB initial stack)
  • Automatic scheduling - Managed by the Go runtime scheduler
  • Efficient communication - Designed to work seamlessly with channels
  • Scalable design - Easy to create thousands of concurrent goroutines
  • Simple syntax - Just add go keyword before a function call

Goroutine Characteristics

Lightweight Threads

Goroutines are much more efficient than traditional OS threads.

Automatic Scheduling

The Go runtime scheduler manages goroutine execution across OS threads.

Communication Focus

Goroutines are designed to communicate through channels rather than shared memory.

Simple Creation

Creating a goroutine is as simple as adding the go keyword.

Creating Goroutines

Basic Goroutine Creation

The go Keyword

The go keyword creates a new goroutine for concurrent execution.

Function Call Syntax

Goroutines are created by adding go before a function call.

package main

import (
"fmt"
"time"
)

func main() {
// Basic goroutine creation examples
fmt.Println("Basic goroutine creation examples:")

// Simple function
func sayHello() {
fmt.Println("Hello from goroutine!")
}

// Create goroutine
go sayHello()

// Give goroutine time to execute
time.Sleep(100 * time.Millisecond)
fmt.Println("Main function continues")
// Output:
// Hello from goroutine!
// Main function continues

// Goroutine with parameters
func greet(name string) {
fmt.Printf("Hello, %s from goroutine!\n", name)
}

// Create goroutine with parameters
go greet("Alice")
go greet("Bob")
go greet("Charlie")

// Give goroutines time to execute
time.Sleep(100 * time.Millisecond)
fmt.Println("All greetings completed")
// Output:
// Hello, Alice from goroutine!
// Hello, Bob from goroutine!
// Hello, Charlie from goroutine!
// All greetings completed

// Anonymous function goroutine
go func() {
fmt.Println("Hello from anonymous goroutine!")
}()

// Give goroutine time to execute
time.Sleep(100 * time.Millisecond)
fmt.Println("Anonymous goroutine completed")
// Output:
// Hello from anonymous goroutine!
// Anonymous goroutine completed

// Anonymous function with parameters
go func(name string, age int) {
fmt.Printf("Name: %s, Age: %d\n", name, age)
}("David", 25)

// Give goroutine time to execute
time.Sleep(100 * time.Millisecond)
fmt.Println("Parameterized anonymous goroutine completed")
// Output:
// Name: David, Age: 25
// Parameterized anonymous goroutine completed

// Multiple goroutines
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d finished\n", id)
}

// Create multiple goroutines
for i := 1; i <= 3; i++ {
go worker(i)
}

// Give goroutines time to execute
time.Sleep(500 * time.Millisecond)
fmt.Println("All workers completed")
// Output:
// Worker 1 starting
// Worker 2 starting
// Worker 3 starting
// Worker 1 finished
// Worker 2 finished
// Worker 3 finished
// All workers completed
}

Goroutine Lifecycle

Goroutine Execution

Understanding how goroutines start, run, and terminate.

Main Goroutine

The main function runs in the main goroutine.

package main

import (
"fmt"
"time"
)

func main() {
// Goroutine lifecycle examples
fmt.Println("Goroutine lifecycle examples:")

// Goroutine that runs and completes
func shortTask() {
fmt.Println("Short task starting")
time.Sleep(50 * time.Millisecond)
fmt.Println("Short task completed")
}

go shortTask()
time.Sleep(100 * time.Millisecond)
fmt.Println("Short task goroutine finished")
// Output:
// Short task starting
// Short task completed
// Short task goroutine finished

// Goroutine that runs longer
func longTask() {
fmt.Println("Long task starting")
for i := 1; i <= 5; i++ {
fmt.Printf("Long task step %d\n", i)
time.Sleep(100 * time.Millisecond)
}
fmt.Println("Long task completed")
}

go longTask()
time.Sleep(600 * time.Millisecond)
fmt.Println("Long task goroutine finished")
// Output:
// Long task starting
// Long task step 1
// Long task step 2
// Long task step 3
// Long task step 4
// Long task step 5
// Long task completed
// Long task goroutine finished

// Goroutine with early termination
func taskWithEarlyReturn() {
fmt.Println("Task with early return starting")
time.Sleep(50 * time.Millisecond)
fmt.Println("Task with early return: step 1")
time.Sleep(50 * time.Millisecond)
fmt.Println("Task with early return: step 2")
return // Early return
fmt.Println("Task with early return: step 3 (never reached)")
}

go taskWithEarlyReturn()
time.Sleep(200 * time.Millisecond)
fmt.Println("Task with early return goroutine finished")
// Output:
// Task with early return starting
// Task with early return: step 1
// Task with early return: step 2
// Task with early return goroutine finished

// Goroutine with loop
func taskWithLoop() {
fmt.Println("Task with loop starting")
for i := 1; i <= 3; i++ {
fmt.Printf("Task with loop: iteration %d\n", i)
time.Sleep(100 * time.Millisecond)
}
fmt.Println("Task with loop completed")
}

go taskWithLoop()
time.Sleep(400 * time.Millisecond)
fmt.Println("Task with loop goroutine finished")
// Output:
// Task with loop starting
// Task with loop: iteration 1
// Task with loop: iteration 2
// Task with loop: iteration 3
// Task with loop completed
// Task with loop goroutine finished
}

Goroutine Communication

Basic Communication Patterns

While possible, sharing variables between goroutines is not recommended.

Using channels for safe communication between goroutines.

package main

import (
"fmt"
"time"
)

func main() {
// Goroutine communication examples
fmt.Println("Goroutine communication examples:")

// Shared variable example (not recommended)
var counter int

func incrementCounter() {
for i := 0; i < 5; i++ {
counter++
fmt.Printf("Counter incremented to: %d\n", counter)
time.Sleep(100 * time.Millisecond)
}
}

go incrementCounter()
time.Sleep(600 * time.Millisecond)
fmt.Printf("Final counter value: %d\n", counter)
// Output:
// Counter incremented to: 1
// Counter incremented to: 2
// Counter incremented to: 3
// Counter incremented to: 4
// Counter incremented to: 5
// Final counter value: 5

// Channel communication (recommended)
messages := make(chan string)

func sendMessage(msg string) {
messages <- msg
fmt.Printf("Sent message: %s\n", msg)
}

func receiveMessage() {
msg := <-messages
fmt.Printf("Received message: %s\n", msg)
}

// Send message in goroutine
go sendMessage("Hello from goroutine!")

// Receive message in main goroutine
receiveMessage()
// Output:
// Sent message: Hello from goroutine!
// Received message: Hello from goroutine!

// Multiple messages
func sendMultipleMessages() {
messages := []string{"Message 1", "Message 2", "Message 3"}
for _, msg := range messages {
messages <- msg
fmt.Printf("Sent: %s\n", msg)
time.Sleep(100 * time.Millisecond)
}
close(messages)
}

func receiveMultipleMessages() {
for msg := range messages {
fmt.Printf("Received: %s\n", msg)
time.Sleep(100 * time.Millisecond)
}
}

// Create new channel for multiple messages
multiMessages := make(chan string)

go func() {
messages := []string{"Message 1", "Message 2", "Message 3"}
for _, msg := range messages {
multiMessages <- msg
fmt.Printf("Sent: %s\n", msg)
time.Sleep(100 * time.Millisecond)
}
close(multiMessages)
}()

// Receive multiple messages
for msg := range multiMessages {
fmt.Printf("Received: %s\n", msg)
time.Sleep(100 * time.Millisecond)
}
// Output:
// Sent: Message 1
// Received: Message 1
// Sent: Message 2
// Received: Message 2
// Sent: Message 3
// Received: Message 3

// Bidirectional communication
func bidirectionalCommunication() {
requestChan := make(chan string)
responseChan := make(chan string)

// Goroutine that processes requests
go func() {
for req := range requestChan {
fmt.Printf("Processing request: %s\n", req)
response := fmt.Sprintf("Response to: %s", req)
responseChan <- response
}
}()

// Send requests
requests := []string{"Request 1", "Request 2", "Request 3"}
for _, req := range requests {
requestChan <- req
fmt.Printf("Sent request: %s\n", req)
}
close(requestChan)

// Receive responses
for i := 0; i < len(requests); i++ {
response := <-responseChan
fmt.Printf("Received response: %s\n", response)
}
close(responseChan)
}

bidirectionalCommunication()
// Output:
// Sent request: Request 1
// Processing request: Request 1
// Received response: Response to: Request 1
// Sent request: Request 2
// Processing request: Request 2
// Received response: Response to: Request 2
// Sent request: Request 3
// Processing request: Request 3
// Received response: Response to: Request 3
}

Goroutine Synchronization

Waiting for Goroutines

Using channels and other mechanisms to wait for goroutines to complete.

Synchronization Patterns

Common patterns for coordinating goroutine execution.

package main

import (
"fmt"
"time"
)

func main() {
// Goroutine synchronization examples
fmt.Println("Goroutine synchronization examples:")

// Simple synchronization with channel
done := make(chan bool)

func worker(done chan bool) {
fmt.Println("Worker starting")
time.Sleep(200 * time.Millisecond)
fmt.Println("Worker finished")
done <- true
}

go worker(done)

// Wait for worker to complete
<-done
fmt.Println("Worker completed, main continues")
// Output:
// Worker starting
// Worker finished
// Worker completed, main continues

// Multiple goroutines synchronization
func multipleWorkers() {
numWorkers := 3
done := make(chan bool, numWorkers)

for i := 1; i <= numWorkers; i++ {
go func(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d finished\n", id)
done <- true
}(i)
}

// Wait for all workers to complete
for i := 0; i < numWorkers; i++ {
<-done
}
fmt.Println("All workers completed")
}

multipleWorkers()
// Output:
// Worker 1 starting
// Worker 2 starting
// Worker 3 starting
// Worker 1 finished
// Worker 2 finished
// Worker 3 finished
// All workers completed

// Goroutine with result
func workerWithResult(id int, resultChan chan<- int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(100 * time.Millisecond)
result := id * 10
fmt.Printf("Worker %d finished with result: %d\n", id, result)
resultChan <- result
}

func collectResults() {
numWorkers := 3
resultChan := make(chan int, numWorkers)

// Start workers
for i := 1; i <= numWorkers; i++ {
go workerWithResult(i, resultChan)
}

// Collect results
var total int
for i := 0; i < numWorkers; i++ {
result := <-resultChan
total += result
fmt.Printf("Collected result: %d\n", result)
}

fmt.Printf("Total result: %d\n", total)
}

collectResults()
// Output:
// Worker 1 starting
// Worker 2 starting
// Worker 3 starting
// Worker 1 finished with result: 10
// Worker 2 finished with result: 20
// Worker 3 finished with result: 30
// Collected result: 10
// Collected result: 20
// Collected result: 30
// Total result: 60

// Goroutine with error handling
func workerWithError(id int, resultChan chan<- int, errorChan chan<- error) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(100 * time.Millisecond)

// Simulate error for worker 2
if id == 2 {
errorChan <- fmt.Errorf("worker %d failed", id)
return
}

result := id * 10
fmt.Printf("Worker %d finished with result: %d\n", id, result)
resultChan <- result
}

func collectResultsWithErrors() {
numWorkers := 3
resultChan := make(chan int, numWorkers)
errorChan := make(chan error, numWorkers)

// Start workers
for i := 1; i <= numWorkers; i++ {
go workerWithError(i, resultChan, errorChan)
}

// Collect results and errors
var results []int
var errors []error

for i := 0; i < numWorkers; i++ {
select {
case result := <-resultChan:
results = append(results, result)
fmt.Printf("Collected result: %d\n", result)
case err := <-errorChan:
errors = append(errors, err)
fmt.Printf("Collected error: %v\n", err)
}
}

fmt.Printf("Results: %v\n", results)
fmt.Printf("Errors: %v\n", errors)
}

collectResultsWithErrors()
// Output:
// Worker 1 starting
// Worker 2 starting
// Worker 3 starting
// Worker 1 finished with result: 10
// Worker 3 finished with result: 30
// Collected result: 10
// Collected result: 30
// Collected error: worker 2 failed
// Results: [10 30]
// Errors: [worker 2 failed]
}

Goroutine Management

Goroutine Lifecycle Management

Starting Goroutines

Understanding when and how to start goroutines.

Stopping Goroutines

Managing goroutine termination and cleanup.

package main

import (
"context"
"fmt"
"time"
)

func main() {
// Goroutine management examples
fmt.Println("Goroutine management examples:")

// Goroutine with context cancellation
func goroutineWithContext() {
ctx, cancel := context.WithCancel(context.Background())

go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine cancelled")
return
default:
fmt.Println("Goroutine working...")
time.Sleep(100 * time.Millisecond)
}
}
}()

// Let goroutine run for a bit
time.Sleep(300 * time.Millisecond)

// Cancel the goroutine
fmt.Println("Cancelling goroutine...")
cancel()

// Give goroutine time to cleanup
time.Sleep(100 * time.Millisecond)
fmt.Println("Goroutine management completed")
}

goroutineWithContext()
// Output:
// Goroutine working...
// Goroutine working...
// Goroutine working...
// Cancelling goroutine...
// Goroutine cancelled
// Goroutine management completed

// Goroutine with timeout
func goroutineWithTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()

go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine timed out")
return
default:
fmt.Println("Goroutine working...")
time.Sleep(100 * time.Millisecond)
}
}
}()

// Wait for timeout
time.Sleep(300 * time.Millisecond)
fmt.Println("Timeout goroutine management completed")
}

goroutineWithTimeout()
// Output:
// Goroutine working...
// Goroutine working...
// Goroutine timed out
// Timeout goroutine management completed

// Goroutine with cleanup
func goroutineWithCleanup() {
cleanup := make(chan bool)

go func() {
defer func() {
fmt.Println("Goroutine cleanup: closing resources")
cleanup <- true
}()

fmt.Println("Goroutine starting work")
time.Sleep(200 * time.Millisecond)
fmt.Println("Goroutine finished work")
}()

// Wait for cleanup
<-cleanup
fmt.Println("Cleanup completed")
}

goroutineWithCleanup()
// Output:
// Goroutine starting work
// Goroutine finished work
// Goroutine cleanup: closing resources
// Cleanup completed

// Multiple goroutines with individual management
func multipleGoroutinesWithManagement() {
numGoroutines := 3
done := make(chan int, numGoroutines)

for i := 1; i <= numGoroutines; i++ {
go func(id int) {
defer func() {
fmt.Printf("Goroutine %d cleanup\n", id)
done <- id
}()

fmt.Printf("Goroutine %d starting\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Goroutine %d finished\n", id)
}(i)
}

// Wait for all goroutines to complete
for i := 0; i < numGoroutines; i++ {
id := <-done
fmt.Printf("Goroutine %d completed\n", id)
}
fmt.Println("All goroutines managed successfully")
}

multipleGoroutinesWithManagement()
// Output:
// Goroutine 1 starting
// Goroutine 2 starting
// Goroutine 3 starting
// Goroutine 1 finished
// Goroutine 1 cleanup
// Goroutine 1 completed
// Goroutine 2 finished
// Goroutine 2 cleanup
// Goroutine 2 completed
// Goroutine 3 finished
// Goroutine 3 cleanup
// Goroutine 3 completed
// All goroutines managed successfully
}

Goroutine Best Practices

When to Use Goroutines

Understanding appropriate use cases for goroutines.

Common Pitfalls

Avoiding common mistakes when working with goroutines.

package main

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

func main() {
// Goroutine best practices examples
fmt.Println("Goroutine best practices examples:")

// Good: Using goroutines for I/O operations
func ioOperation() {
fmt.Println("Starting I/O operation")
time.Sleep(200 * time.Millisecond) // Simulate I/O
fmt.Println("I/O operation completed")
}

go ioOperation()
time.Sleep(300 * time.Millisecond)
fmt.Println("I/O goroutine completed")
// Output:
// Starting I/O operation
// I/O operation completed
// I/O goroutine completed

// Good: Using goroutines for concurrent processing
func concurrentProcessing() {
data := []int{1, 2, 3, 4, 5}
results := make(chan int, len(data))

for _, item := range data {
go func(value int) {
// Simulate processing
time.Sleep(100 * time.Millisecond)
result := value * 2
results <- result
}(item)
}

// Collect results
for i := 0; i < len(data); i++ {
result := <-results
fmt.Printf("Processed result: %d\n", result)
}
}

concurrentProcessing()
// Output:
// Processed result: 2
// Processed result: 4
// Processed result: 6
// Processed result: 8
// Processed result: 10

// Good: Using sync.WaitGroup for coordination
func waitGroupExample() {
var wg sync.WaitGroup

for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Worker %d finished\n", id)
}(i)
}

wg.Wait()
fmt.Println("All workers completed")
}

waitGroupExample()
// Output:
// Worker 1 starting
// Worker 2 starting
// Worker 3 starting
// Worker 1 finished
// Worker 2 finished
// Worker 3 finished
// All workers completed

// Bad: Creating too many goroutines
func tooManyGoroutines() {
fmt.Println("Creating many goroutines...")

for i := 0; i < 1000; i++ {
go func(id int) {
time.Sleep(1 * time.Millisecond)
// Do minimal work
}(i)
}

time.Sleep(100 * time.Millisecond)
fmt.Println("Many goroutines completed")
}

tooManyGoroutines()
// Output:
// Creating many goroutines...
// Many goroutines completed

// Good: Using worker pools instead
func workerPoolExample() {
numWorkers := 3
jobs := make(chan int, 10)
results := make(chan int, 10)

// Start workers
for i := 1; i <= numWorkers; i++ {
go func(id int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(100 * time.Millisecond)
results <- job * 2
}
}(i)
}

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

// Collect results
for i := 1; i <= 5; i++ {
result := <-results
fmt.Printf("Result: %d\n", result)
}
}

workerPoolExample()
// Output:
// Worker 1 processing job 1
// Worker 2 processing job 2
// Worker 3 processing job 3
// Worker 1 processing job 4
// Worker 2 processing job 5
// Result: 2
// Result: 4
// Result: 6
// Result: 8
// Result: 10

// Good: Proper error handling in goroutines
func goroutineWithErrorHandling() {
errorChan := make(chan error, 1)

go func() {
defer func() {
if r := recover(); r != nil {
errorChan <- fmt.Errorf("panic in goroutine: %v", r)
}
}()

// Simulate work
time.Sleep(100 * time.Millisecond)
fmt.Println("Goroutine completed successfully")
errorChan <- nil
}()

err := <-errorChan
if err != nil {
fmt.Printf("Goroutine error: %v\n", err)
} else {
fmt.Println("Goroutine completed without errors")
}
}

goroutineWithErrorHandling()
// Output:
// Goroutine completed successfully
// Goroutine completed without errors
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's goroutines:

Goroutine Creation

  • Understanding how to create goroutines with the go keyword
  • Working with function calls and anonymous functions in goroutines
  • Creating multiple goroutines for concurrent execution
  • Understanding goroutine syntax and patterns

Goroutine Lifecycle

  • Understanding how goroutines start, run, and terminate
  • Working with goroutine execution and scheduling
  • Managing goroutine lifecycle and cleanup
  • Understanding the relationship between main and goroutines

Goroutine Communication

  • Understanding communication patterns between goroutines
  • Working with channels for safe communication
  • Implementing synchronization patterns
  • Handling errors and results in goroutines

Goroutine Management

  • Managing goroutine lifecycle with context
  • Implementing proper cleanup and termination
  • Using synchronization primitives for coordination
  • Following best practices for goroutine usage

Key Concepts

  • go - Keyword for creating goroutines
  • Goroutines - Lightweight concurrent execution units
  • Communication - Safe communication between goroutines
  • Synchronization - Coordinating goroutine execution
  • Lifecycle - Managing goroutine start, run, and termination

Next Steps

You now have a solid foundation in Go's goroutines. In the next section, we'll explore channels, which are the primary mechanism for communication between goroutines.

Understanding goroutines is crucial for building concurrent applications in Go. These concepts form the foundation for all the more advanced concurrency techniques we'll cover in the coming chapters.


Ready to learn about channels? Let's explore Go's powerful channel system and learn how to enable safe communication between goroutines!