Goroutines and Channels: Concurrency in Go
Master Go concurrency with goroutines, channels, select statements, and common concurrency patterns.
What are Goroutines?
Goroutines are lightweight threads managed by the Go runtime. They are incredibly cheap to create — you can run thousands of goroutines simultaneously with minimal memory overhead.
``go
package main
import ( "fmt" "time" )
func sayHello(name string) { for i := 0; i < 3; i++ { fmt.Printf("Hello from %s (iteration %d)\n", name, i) time.Sleep(100 * time.Millisecond) } }
func main() {
go sayHello("goroutine 1") // Start goroutine
go sayHello("goroutine 2") // Start another
sayHello("main") // Run in main goroutine
}
`
Channels
Channels are Go's way of communicating between goroutines. They provide a safe way to send and receive values.
`go
func main() {
// Create a channel
ch := make(chan string)
// Send value in a goroutine go func() { ch <- "Hello from goroutine!" }()
// Receive value in main
msg := <-ch
fmt.Println(msg) // "Hello from goroutine!"
}
`
Buffered Channels
`go
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 // Would block! Buffer full
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
`
Channel Direction
`go
// Send-only channel
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// Receive-only channel func consumer(ch <-chan int) { for val := range ch { fmt.Println("Received:", val) } }
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
`
Select Statement
Select lets you wait on multiple channel operations, similar to a switch for channels.
`go
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() { time.Sleep(100 * time.Millisecond) ch1 <- "from ch1" }()
go func() { time.Sleep(200 * time.Millisecond) ch2 <- "from ch2" }()
// Wait for the first response
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
}
`
Common Patterns
Fan-Out / Fan-In
`go
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = worker(input)
}
return channels
}
func worker(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output)
for n := range input {
output <- n * n // Process
}
}()
return output
}
`
Worker Pool
`go
func workerPool(jobs <-chan int, results chan<- int, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
`
Context for Cancellation
`go
func longRunningTask(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // Cancelled or timed out
default:
// Do work
time.Sleep(100 * time.Millisecond)
}
}
}
func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel()
if err := longRunningTask(ctx); err != nil {
fmt.Println("Task ended:", err)
}
}
`
sync Package
`go
// Mutex for shared state
var mu sync.Mutex
var count int
func increment() { mu.Lock() defer mu.Unlock() count++ }
// WaitGroup to wait for goroutines
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
`
Best Practices
1. Don't communicate by sharing memory; share memory by communicating (use channels)
2. Always close channels from the sender side
3. Use context for cancellation and timeouts
4. Avoid goroutine leaks — ensure all goroutines can exit
5. Use sync.WaitGroup to wait for goroutine completion
6. Use -race flag during testing: go test -race ./...`