What Are Goroutines?
Goroutines are lightweight threads of execution managed by the Go runtime. Unlike traditional threads, goroutines are extremely efficient and scalable, making it easy to run thousands of them simultaneously.
Key Features
- Lightweight compared to system threads.
- Managed by the Go runtime, not the OS.
- Use the
go
keyword to create a goroutine.
Comparison with Threads: Unlike traditional threads that are OS-managed and consume significant resources, goroutines are designed to be more efficient and provide an abstraction that simplifies concurrency programming.
Syntax
Creating a goroutine is as simple as prefixing a function call with the go
keyword:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from goroutine!")
}
func main() {
go sayHello()
fmt.Println("Main function execution")
time.Sleep(1 * time.Second) // Ensure main doesn't exit before goroutine completes
}
Key Takeaway: The above example shows how the go
keyword enables a function to execute concurrently without blocking the main function.
Examples
Running Multiple Goroutines
package main
import (
"fmt"
"time"
)
func printNumber(number int) {
fmt.Printf("Number: %d\n", number)
}
func main() {
for i := 0; i < 5; i++ {
go printNumber(i)
}
time.Sleep(1 * time.Second) // Allow goroutines to finish
}
Output: Order may vary due to concurrent execution. This illustrates the asynchronous nature of goroutines.
Using Goroutines with Channels
Channels are used to communicate between goroutines:
package main
import "fmt"
func worker(done chan bool) {
fmt.Println("Working...")
done <- true
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Wait for worker to finish
fmt.Println("Done!")
}
Error Handling in Goroutines
Handling errors effectively in goroutines can be achieved by passing error values through channels:
package main
import (
"fmt"
"errors"
)
func riskyTask(errorsChan chan error) {
// Simulate an error
err := errors.New("Something went wrong")
errorsChan <- err
}
func main() {
errorsChan := make(chan error)
go riskyTask(errorsChan)
err := <-errorsChan
if err != nil {
fmt.Printf("Error encountered: %s\n", err)
}
}
Common Pitfalls
1. Goroutine Leaks
Ensure goroutines have a way to exit, such as listening to a channel or using context cancellation. For example:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker exiting...")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(3 * time.Second)
}
2. Race Conditions
Use synchronization primitives like sync.Mutex
to avoid concurrent writes to shared data. For instance:
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}
Best Practices
- Use
sync.WaitGroup
to manage goroutine lifecycles. - Avoid shared state; use channels for communication.
- Always handle errors within goroutines to prevent silent failures.
- Leverage
context
for timeout and cancellation management. - Test concurrent code thoroughly to ensure correctness under high loads.
Example of combining WaitGroup
and context
:
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d exiting...\n", id)
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
wg.Wait()
fmt.Println("All workers completed.")
}