Go Concurrency - Goroutines and Channels
Concurrency is one of Go's strongest features. It lets you run multiple tasks at the same time, making your programs faster and more efficient. Let's learn how to use it!
What is Concurrency?
Concurrency means doing multiple things at the same time. Think of it like a restaurant - one chef can start cooking while another prepares ingredients. Both tasks happen simultaneously!
Concurrency vs Parallelism:
- Concurrency - Managing multiple tasks (can run on one CPU)
- Parallelism - Running tasks simultaneously (needs multiple CPUs)
Go makes concurrency easy with goroutines and channels!
Goroutines
Goroutines are lightweight threads managed by Go. They're super cheap to create - you can have thousands of them!
Basic Goroutine
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello", i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Start goroutine
go sayHello()
// Main continues immediately
fmt.Println("Main function")
time.Sleep(1 * time.Second)
}Multiple Goroutines
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println("Number:", i)
time.Sleep(100 * time.Millisecond)
}
}
func printLetters() {
for i := 'a'; i <= 'e'; i++ {
fmt.Println("Letter:", string(i))
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go printNumbers()
go printLetters()
time.Sleep(1 * time.Second)
}Channels
Channels are how goroutines communicate. Think of them as pipes - you send data in one end and receive it from the other.
Creating Channels
// Unbuffered channel
ch := make(chan int)
// Buffered channel (can hold 5 values)
ch := make(chan int, 5)Sending and Receiving
ch := make(chan string)
// Send data
go func() {
ch <- "Hello" // Send
}()
// Receive data
msg := <-ch // Receive
fmt.Println(msg) // "Hello"Complete Example
package main
import "fmt"
func sendData(ch chan string) {
ch <- "Hello"
ch <- "World"
close(ch) // Close channel when done
}
func main() {
ch := make(chan string)
go sendData(ch)
// Receive data
for msg := range ch {
fmt.Println(msg)
}
}
// Output:
// Hello
// WorldChannel Directions
You can specify channel direction:
// Send-only channel
func sendOnly(ch chan<- string) {
ch <- "Hello"
}
// Receive-only channel
func receiveOnly(ch <-chan string) {
msg := <-ch
fmt.Println(msg)
}Buffered Channels
Buffered channels can hold values without blocking:
ch := make(chan int, 3) // Buffer size 3
ch <- 1 // Doesn't block
ch <- 2 // Doesn't block
ch <- 3 // Doesn't block
ch <- 4 // Blocks until space availableSelect Statement
Select lets you wait on multiple channels:
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}Real-World Example: Worker Pool
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2 // Process job
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for r := 1; r <= 5; r++ {
fmt.Println("Result:", <-results)
}
}WaitGroups
WaitGroups wait for goroutines to finish:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Signal completion
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 <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // Wait for all workers
fmt.Println("All workers done")
}Mutex (Mutual Exclusion)
Protect shared data:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final value:", counter.Value()) // 1000
}Visual Explanation: Goroutines and Channels
Here's how concurrency works:
Goroutines Flow:
┌─────────────────────────────┐
│ Main Goroutine │
│ │ │
│ ├─► Goroutine 1 │
│ ├─► Goroutine 2 │
│ └─► Goroutine 3 │
│ │
│ All run concurrently │
└─────────────────────────────┘
Channel Communication:
┌──────────────┐ ┌──────────────┐
│ Goroutine 1 │ │ Goroutine 2 │
│ │ │ │
│ ch <- data │────────►│ data := <-ch │
│ │ │ │
└──────────────┘ └──────────────┘Frequently Asked Questions (FAQ)
Q1: What's the difference between goroutine and thread?
A:
- Goroutine - Lightweight, managed by Go runtime, cheaper
- Thread - OS-level, heavier, more expensive
Goroutines are much cheaper - you can have millions!
Q2: How do I wait for a goroutine to finish?
A: Use WaitGroup or channels:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// Work
}()
wg.Wait()Q3: What's the difference between buffered and unbuffered channels?
A:
- Unbuffered - Sender blocks until receiver ready
- Buffered - Sender blocks only when buffer full
Q4: Can I close a channel?
A: Yes! Close when sender is done:
close(ch)Receivers can check if channel is closed:
msg, ok := <-ch
if !ok {
// Channel closed
}Q5: What happens if I send to a closed channel?
A: Panic! Always close from sender side, and only once.
Q6: How do I prevent race conditions?
A: Use mutexes or channels to synchronize access to shared data:
var mu sync.Mutex
mu.Lock()
// Critical section
mu.Unlock()Q7: What's a deadlock?
A: When goroutines wait for each other forever:
ch := make(chan int)
ch <- 1 // Blocks forever (no receiver)Q8: Can I use select with default?
A: Yes! Default makes select non-blocking:
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("No message")
}Q9: How many goroutines can I create?
A: Thousands or even millions! Goroutines are very lightweight. Only limited by available memory.
Q10: When should I use channels vs mutexes?
A:
- Channels - Communication between goroutines
- Mutexes - Protecting shared data
Use channels for communication, mutexes for synchronization.
Next Steps
Now that you understand concurrency, here's what to learn next:
- Next: Learn about Go REST API - Build concurrent APIs
- Explore Go Error Handling - Handle errors in goroutines
- Understand Go Context - Cancel and timeout goroutines
- Master Go Best Practices - Write better concurrent code
Concurrency is powerful in Go. Practice using goroutines and channels to get comfortable!