Goroutines and Concurrency
50 minGoroutines are lightweight threads managed by the Go runtime, enabling concurrent execution with minimal overhead. Starting a goroutine is as simple as `go function()`. Goroutines are multiplexed onto OS threads by the Go scheduler, making them very efficient. You can have thousands of goroutines running concurrently. Understanding goroutines is essential for writing concurrent Go programs.
Channels provide communication between goroutines, enabling safe data sharing. Channels are typed conduits that allow sending and receiving values. Channels can be buffered (with capacity) or unbuffered (synchronous). Channels are the primary way goroutines communicate. Understanding channels is fundamental to Go's concurrency model.
The `select` statement handles multiple channel operations, allowing goroutines to wait on multiple channels simultaneously. Select is like a switch for channelsāit chooses a case that can proceed. Select enables non-blocking operations and timeouts. Understanding select helps you coordinate multiple goroutines effectively.
Go's concurrency model is based on CSP (Communicating Sequential Processes), where goroutines communicate by sending messages through channels rather than sharing memory. The motto is 'Don't communicate by sharing memory; share memory by communicating.' This model makes concurrent programs safer and easier to reason about. Understanding CSP helps you write correct concurrent code.
Synchronization primitives include channels (primary), sync.Mutex for mutual exclusion, sync.WaitGroup for waiting on goroutines, and sync.Once for one-time initialization. Channels are preferred for communication; mutexes for protecting shared state. Understanding synchronization helps you write safe concurrent programs.
Best practices include using channels for communication, avoiding shared mutable state, using select for coordination, closing channels to signal completion, and using context.Context for cancellation. Concurrency should be used when it improves performance or responsiveness. Understanding goroutines and concurrency enables you to write efficient, scalable Go programs.
Key Concepts
- Goroutines are lightweight threads managed by the Go runtime.
- Channels provide communication between goroutines.
- select handles multiple channel operations.
- Go's concurrency model is based on CSP (Communicating Sequential Processes).
- Channels are preferred over shared memory for communication.
Learning Objectives
Master
- Creating and managing goroutines for concurrent execution
- Using channels for goroutine communication
- Using select for coordinating multiple channels
- Understanding Go's CSP concurrency model
Develop
- Concurrent programming thinking
- Understanding when and how to use concurrency
- Designing safe, efficient concurrent systems
Tips
- Use channels for communicationāprefer communication over shared memory.
- Use select to coordinate multiple goroutines.
- Close channels to signal completion.
- Use context.Context for cancellation and timeouts.
Common Pitfalls
- Sharing mutable state between goroutines without synchronization.
- Not closing channels, causing goroutine leaks.
- Deadlocks from improper channel usage.
- Creating too many goroutines, causing resource exhaustion.
Summary
- Goroutines enable lightweight concurrent execution.
- Channels provide safe communication between goroutines.
- select coordinates multiple channel operations.
- Go's CSP model makes concurrent programs safer and easier to reason about.
- Understanding concurrency is essential for scalable Go programs.
Exercise
Demonstrate goroutines, channels, and concurrent programming patterns.
package main
import (
"fmt"
"sync"
"time"
)
// Simple goroutine
func sayHello(name string) {
fmt.Printf("Hello, %s!
", name)
}
// Goroutine with channel communication
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d
", id, job)
time.Sleep(time.Millisecond * 500) // Simulate work
results <- job * 2
}
}
// Function that uses select
func selectExample(ch1, ch2 chan string) {
for i := 0; i < 4; i++ {
select {
case msg1 := <-ch1:
fmt.Printf("Received from ch1: %s
", msg1)
case ch2 <- "message":
fmt.Println("Sent message to ch2")
case <-time.After(time.Second):
fmt.Println("Timeout")
}
}
}
// WaitGroup example
func processWithWaitGroup(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("Worker %d starting
", id)
time.Sleep(time.Millisecond * 100)
fmt.Printf("Worker %d done
", id)
}
func main() {
// Simple goroutine
go sayHello("Goroutine")
time.Sleep(time.Millisecond * 100)
// Channel communication
jobs := make(chan int, 5)
results := make(chan int, 5)
// Start workers
for i := 1; i <= 3; i++ {
go worker(i, jobs, results)
}
// 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
", result)
}
// Select example
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Millisecond * 500)
ch1 <- "Hello from ch1"
}()
go func() {
time.Sleep(time.Millisecond * 300)
<-ch2
}()
selectExample(ch1, ch2)
// WaitGroup example
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go processWithWaitGroup(&wg, i)
}
wg.Wait()
fmt.Println("All workers completed")
// Buffered channels
buffered := make(chan int, 3)
buffered <- 1
buffered <- 2
buffered <- 3
close(buffered)
for value := range buffered {
fmt.Printf("Buffered channel value: %d
", value)
}
}